3. spring security & oauth2

上一次写到使用spring-security做简单登录应用,先补交家庭作业

如何自定义登录页面#####

  1. 修改WebSecurityConfig
@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/user/**").hasAnyRole("ADMIN", "USER")
                .and()
                .formLogin().loginPage("/login").loginProcessingUrl("/login.do").defaultSuccessUrl("/user/info")
                .failureUrl("/login?err=1")
                .permitAll()
                .and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/")
                .permitAll()
        ;

    }

解释看代码字面意思就懂了,没什么特殊的,还可以修改比如登录表单里的用户名和密码的名字,还可以添加各种登录成功之后的handler等等,写法都一样。

  1. 添加LoginController
@Controller
public class LoginController {

    @RequestMapping("/login")
    public String login(String err, ModelMap modelMap) {
        if (StringUtils.hasLength(err)) {
            modelMap.put("err", err);
        }
        return "login";
    }
}
  1. 添加页面就是一个普通的form表单
<form action="/login.do" method="post">
            <input type="hidden" th:value="${_csrf.token}" name="_csrf"/>
            <div class="form-group">
                <label for="usernameInput">Username</label>
                <input type="text" id="usernameInput" class="form-control" name="username" placeholder="enter your username"/>
            </div>
            <div class="form-group">
                <label for="passwordInput">Password</label>
                <input type="password" id="passwordInput" class="form-control" name="password" placeholder="enter your password"/>
            </div>
            <div class="form-group" th:if="${err}">
                <div class="text-danger ">
                    用户名或密码错误
                </div>
            </div>
            <div class="form-group text-center">
                <button type="submit" class="btn btn-primary">Login</button>
                <button type="reset" class="btn btn-warning">Reset</button>
            </div>
        </form>

这里需要注意的就是表单里有一个隐藏的input叫_csrf是用来防范跨域攻击的,详情查看CSRF。可以在配置类关掉.csrf().disable(),当然是不推荐的。
以上代码地址:v1.0

oauth2#####

上面的代码应该满足一般意义上的网站登录,下面简单介绍怎么使用spring-security-oauth这东西比较复杂,分两部分,先讲一部分。

至于OAuth是什么东西,请问百度或谷歌,官方地址在https://oauth.net/2/
OAuth简单来说是一种协议。在我看来就是对上篇文章所说的登录鉴权模式的一种补充(当然不是严格意义的说,我只是想简单明了的阐述一下这是一个什么东西),大家都知道以前系统是分为两种模式的——B/SC/S,上篇文章讲述的都是B/S模式下的登录授权方式,但是对于C端即Client是不好使的,所以就有了OAuth协议(来历纯属虚构),那么什么情况下会使用呢?——虽然感觉很神秘,但是这个协议应用却很常见——几乎所有的大公司都在使用,比如微信登录微博登录GitHub登录Google登录QQ登录FaceBook登录Twitter登录等等这种第三方登录都是使用的这个协议,可以看看微博登录的地址即可:

https://api.weibo.com/oauth2/authorize?scope=email&state=f4dde19fcea5b2ab8c3e682da17a511d&redirect_uri=http%3A%2F%2Fwww.zhihu.com%2Foauth%2Fcallback%2Fsina&response_type=code&client_id=3063806388
oauth2协议流程######

在网上盗的别人的一个图:

OAuth2 认证
  1. Client向服务器发起OAuth请求交换authorization_code,需要携带的参数:
    • client_id:可以理解为客户端用于登录的用户名
    • response_type:本次请求什么?一般首次请求为code
    • redirect_uri:认证成功返回的地址
    • scope:权限范围,指本次授权获取资源的权限范围,比如只读,可读写之类
    • state:一般为随机数,可选,服务器会原样返回,用于客户端验证服务器
      这些参数名称以及值不是严格意义不变的,各个认证服务器都会有自己的风格。
  2. 如果是浏览器发起的第三方登录,比如上述举例的在知乎上使用微博登录,输入微博的用户名和密码,验证通过之后,则服务器会自动从微博重定向到刚才的redirect_uri,严谨一点的服务器还会询问你是否允许比如知乎请求你的微博个人信息。如果不是浏览器比如手机APP,服务器会直接返回code
  3. 使用code获取access_token,这一步一般是在客户端的服务器(不是第三方认证服务器,比如上文的知乎浏览器即客户端,知乎的服务器即客户端的服务器,微博即第三方认证服务器)进行的,就是一般来说用户是无感知的(不是所有的认证服务器都有这一步,只是更加安全而已,服务器实现方式不同,有的是直接可以使用用户名以及密码换取access_token的),一般需要携带的参数:
    • client_id:如上
    • client_secret:可以理解为客户端的密码,做过APP的肯定都接触过使用微博登录的话是需要在微博申请app_idapp_secret的,其实对应就是client_idclient_secret
    • code: 第一步请求获取的authorization_code
    • grant_type:认证类型,也可以理解为本次请求需要做什么,这个属性各个服务器定义非常不同,都不是按照OAuth标准协议来的,各有各的任性,不过通常意义上都实现了下面五种类型:
      • authorization_code:即本次讲解的流程,需要先请求code,再用codetoken
      • password:直接使用用户名和密码交换token
      • refresh_token:刷新token
      • implicit:简单模式,一般用在JS等直接在浏览器里获取token的方式,当然密码也是直接放在参数里的,尽管加了密,想想也不太安全(经 @251be0f15727 纠正请看评论),很少用
      • client_credentials:无用户模式,即直接客户端的服务器单凭自己的client_idclient_secret请求资源,一般用于请求一些服务器的非私密信息,使用极少
    • 如果不是浏览器,还需要加上usernamepassword即用户的登录信息

上述1,2,3举得是标准全量流程,使用不同的客户端方式可能会有不同,但是大的流程是不变的。获取到access_token后就可以使用token向微博请求个人信息了,比如昵称和头像;也可以使用token刷新access_token

使用GitHub登录实战演练#####

先演示如何搭建客户端,选择GitHub作为OAuth认证服务器,需要在GitHub Settings注册客户端

注册测试客户端

演示所用客户端配置如图
localhost配置

需要注意的参数主要是callback URL,需要和传递的参数完全一致,对应上面的redirect_uri解释。

修改WebSecurityConfig

    @Autowired
    OAuth2ClientContext oauth2ClientContext;
...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/user/**").authenticated()
                .anyRequest().permitAll()
                .and().exceptionHandling()
                .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
//                .and()
//                .formLogin().loginPage("/login").loginProcessingUrl("/login.do").defaultSuccessUrl("/user/info")
//                .failureUrl("/login?err=1")
//                .permitAll()
                .and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/")
                .permitAll()

                .and().addFilterBefore(sso(), BasicAuthenticationFilter.class)
        ;

    }

    private Filter sso() {
        OAuth2ClientAuthenticationProcessingFilter githubFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/github");
        OAuth2RestTemplate githubTemplate = new OAuth2RestTemplate(github(), oauth2ClientContext);
        githubFilter.setRestTemplate(githubTemplate);
        githubFilter.setTokenServices(new UserInfoTokenServices(githubResource().getUserInfoUri(), github().getClientId()));
        return githubFilter;
    }

    @Bean
    @ConfigurationProperties("github.resource")
    public ResourceServerProperties githubResource() {
        return new ResourceServerProperties();
    }

    @Bean
    @ConfigurationProperties("github.client")
    public AuthorizationCodeResourceDetails github() {
        return new AuthorizationCodeResourceDetails();
    }

    @Bean
    public FilterRegistrationBean oauth2ClientFilterRegistration(
            OAuth2ClientContextFilter filter) {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(filter);
        registration.setOrder(-100);
        return registration;
    }

这就是主要代码了,标准流程spring-security-oauth2已经都帮我们写好了,代码解释:

  1. addFilterBefore(sso(), BasicAuthenticationFilter.class)在网站基本认证之前添加OAuth2ClientAuthenticationProcessingFilter,上面介绍的OAuth流程基本都在这个Filter里,建议翻看源码有助于理解
  2. @ConfigurationProperties注解是方便通过配置文件生成所需类,这里因为OAuth配置比较复杂,故将properties文件改用YAML模式,具体配置下面讲解。
  3. 注册一个额外的FilterOAuth2ClientContextFilter,主要作用是重定向,当遇到需要权限的页面或URL,代码抛出异常,这时这个Filter将重定向到OAuth鉴权的地址,本文即/login/github
  4. 之后向GitHub获取code包括token的功能spring都帮我们封装好了,非常方便,如果要自定义,只要重写对应的接口即可,就像上篇文章里重写UserDetailsService是一样的

配置文件:

github:
  client:
    clientId: fd57ea20f71057e0f396
    clientSecret: 30b73085b6c726b5fb2e0fa8402846b72d86451f
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    authenticationScheme: query
    clientAuthenticationScheme: form
#    pre-established-redirect-uri: http://localhost:8090/login/github
#    registered-redirect-uri: http://localhost:8090/login/github
#    use-current-uri: false
  resource:
    userInfoUri: https://api.github.com/user

clientIdclientSecret在上文注册客户端的Github页面上有,accessTokenUri是获取token的地址,userAuthorizationUri是登陆之后询问的地址,只是第一次会有,张这样:

询问

使用过微博登录的同学肯定也见过微博的这个页面,现在大家应该知道这个页面是怎么来了的吧,后面2个Schema是指请求参数以什么样的方式跟随,有query(跟在url参数后面),form(以form的body形式提交),header(放到Http header里),none(没有,我也不知道这个是什么意思),注释的3个是测试回调地址的,默认是将当前url地址作为redirect_uri,因为有些服务器是允许多个回调地址的,这个具体看服务器自己怎么设定。这些参数都可以在对应ResourceServerPropertiesAuthorizationCodeResourceDetails的源码找到。


最后,页面代码就不贴了,就是一个a标签,链接到/login/github,运行成功的话,在user页面应该可以看到你的github的用户名

kaenry使用github登录后的页面

本文到此结束,再写实在太长了,下文将介绍如何使用spring-security-oauth2OAuth Server,我想这才是重点。另外提一点,上面的代码不完整,只支持github登录(有兴趣的同学可以添加诸如FaceBook登录啊什么的,改改配置文件和地址就行了),本来的admin/admin将失去效果,可以使用ajax提交form的形式代替,就几行代码:

@RequestMapping(value = "/ajaxLogin", method = RequestMethod.POST)
    @ResponseBody
    public String ajaxLogin(@RequestParam String username, @RequestParam String password) {
        try {
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
            token.setDetails(loginService.loadUserByUsername(username));
            Authentication auth = authenticationManager.authenticate(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
            return JsonResponse.returnResult(true);
        } catch (UsernameNotFoundException e1) {
            LOGGER.error("ajax login error, username not found", e1);
            return JsonResponse.returnMsg(false,"login.passwordError");
        } catch (BadCredentialsException e2) {
            LOGGER.error("ajax login error, password not right", e2);
            return JsonResponse.returnMsg(false,"login.passwordError");
        } catch (Exception e){
            LOGGER.error("ajax login error. forbidden login status", e);
            return JsonResponse.returnMsg(false,"login.forbidden");
        }
    }

这里后面会搭建OAuth server就不赘述了,代码打了taggithub v1.2

PS:上面的代码其实为了方遍理解OAuth流程,spring有个注解叫@EnableOAuth2Sso(这里用的是它的子集EnableOAuth2Client)可以一键搞定,一行代码都不用写,有兴趣的可以看一看。.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容