• Spring Boot 2.0 利用 Spring Security 实现简单的OAuth2.0认证方式2


    0.前言

      经过前面一小节已经基本配置好了基于SpringBoot+SpringSecurity+OAuth2.0的环境。这一小节主要对一些写固定InMemory的User和Client进行扩展。实现动态查询用户,但为了演示方便,这里没有查询数据库。仅做Demo演示,最最关键的是,作为我个人笔记。其实代码里面有些注释,可能只有我知道为什么,有些是Debug调试时的一些测试代码。还是建议,读者自己跑一遍会比较好,能跟深入的理解OAuth2.0协议。我也是参考网上很多博客,然后慢慢测试和理解的。
      参考的每个人的博客,都写得很好很仔细,但是有些关键点,还是要自己写个Demo出来才会更好理解。
      结合数据库的,期待下一篇博客

    1.目录结构

      
      SecurityConfiguration.java Spring-Security 配置
      auth/BaseClientDetailService.java 自定义客户端认证
      auth/BaseUserDetailService.java 自定义用户认证
      integration/* 通过过滤器方式对OAuth2.0集成多种认证方式
      model/SysGrantedAuthority.java 授权权限模型
      model/SysUserAuthentication.java 认证用户主体模型
      server/AuthorizationServerConfiguration.java OAuth 授权服务器配置
      server/ResourceServerConfiguration.java OAuth 资源服务器配置

    2.代码解析

    (1) SecurityConfiguration.java

     1 /**
     2  * Spring-Security 配置<br>
     3  * 具体参考: https://github.com/lexburner/oauth2-demo
     4  * http://blog.didispace.com/spring-security-oauth2-xjf-1/ 
     5  * https://www.cnblogs.com/cjsblog/p/9152455.html
     6  * https://segmentfault.com/a/1190000014371789 (多种认证方式)
     7  * @author wunaozai
     8  * @date 2018-05-28
     9  */
    10 @Configuration
    11 @EnableWebSecurity
    12 @EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法级的权限认证
    13 public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    14     
    15     //通过自定义userDetailsService 来实现查询数据库,手机,二维码等多种验证方式
    16     @Bean
    17     @Override
    18     protected UserDetailsService userDetailsService(){
    19         //采用一个自定义的实现UserDetailsService接口的类
    20         return new BaseUserDetailService();
    21         /*
    22         InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    23         BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    24         String finalPassword = "{bcrypt}"+bCryptPasswordEncoder.encode("123456");
    25         manager.createUser(User.withUsername("user_1").password(finalPassword).authorities("USER").build());
    26         finalPassword = "{noop}123456";
    27         manager.createUser(User.withUsername("user_2").password(finalPassword).authorities("USER").build());
    28         return manager;
    29         */
    30     }
    31 
    32     @Override
    33     protected void configure(HttpSecurity http) throws Exception {
    34 //        http.authorizeRequests()
    35 //            .antMatchers("/", "/index.html", "/oauth/**").permitAll() //允许访问
    36 //            .anyRequest().authenticated() //其他地址的访问需要验证权限
    37 //            .and()
    38 //            .formLogin()
    39 //            .loginPage("/login.html") //登录页
    40 //            .failureUrl("/login-error.html").permitAll()
    41 //            .and()
    42 //            .logout()
    43 //            .logoutSuccessUrl("/index.html");
    44         http.authorizeRequests().anyRequest().fullyAuthenticated();
    45         http.formLogin().loginPage("/login").failureUrl("/login?code=").permitAll();
    46         http.logout().permitAll();
    47         http.authorizeRequests().antMatchers("/oauth/authorize").permitAll();
    48     }
    49     
    50     /**
    51      * 用户验证
    52      */
    53     @Override
    54     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    55         super.configure(auth);
    56     }
    57     
    58     /**
    59      * Spring Boot 2 配置,这里要bean 注入
    60      */
    61     @Bean
    62     @Override
    63     public AuthenticationManager authenticationManagerBean() throws Exception {
    64         AuthenticationManager manager = super.authenticationManagerBean();
    65         return manager;
    66     }
    67     
    68     @Bean
    69     PasswordEncoder passwordEncoder() {
    70         return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    71     }
    72 }

    (2) AuthorizationServerConfiguration.java

     1 /**
     2  * OAuth 授权服务器配置
     3  * https://segmentfault.com/a/1190000014371789
     4  * @author wunaozai
     5  * @date 2018-05-29
     6  */
     7 @Configuration
     8 @EnableAuthorizationServer
     9 public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    10     
    11     private static final String DEMO_RESOURCE_ID = "order";
    12     
    13     @Autowired
    14     AuthenticationManager authenticationManager;
    15     @Autowired
    16     RedisConnectionFactory redisConnectionFactory;
    17     
    18     @Override
    19     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    20         //String finalSecret = "{bcrypt}"+new BCryptPasswordEncoder().encode("123456");
    21         //clients.setBuilder(builder);
    22         //这里通过实现 ClientDetailsService接口
    23         clients.withClientDetails(new BaseClientDetailService());
    24         /*
    25         //配置客户端,一个用于password认证一个用于client认证
    26         clients.inMemory()
    27             .withClient("client_1")
    28             .resourceIds(DEMO_RESOURCE_ID)
    29             .authorizedGrantTypes("client_credentials", "refresh_token")
    30             .scopes("select")
    31             .authorities("oauth2")
    32             .secret(finalSecret)
    33             .and()
    34             .withClient("client_2")
    35             .resourceIds(DEMO_RESOURCE_ID)
    36             .authorizedGrantTypes("password", "refresh_token")
    37             .scopes("select")
    38             .authorities("oauth2")
    39             .secret(finalSecret)
    40             .and()
    41             .withClient("client_code")
    42             .resourceIds(DEMO_RESOURCE_ID)
    43             .authorizedGrantTypes("authorization_code", "client_credentials", "refresh_token",
    44                     "password", "implicit")
    45             .scopes("all")
    46             //.authorities("oauth2")
    47             .redirectUris("http://www.baidu.com")
    48             .accessTokenValiditySeconds(1200)
    49             .refreshTokenValiditySeconds(50000);
    50             */
    51     }
    52 
    53     @Override
    54     public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    55         endpoints
    56                 .tokenStore(new RedisTokenStore(redisConnectionFactory))
    57                 .authenticationManager(authenticationManager)
    58                 .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
    59         
    60         //配置TokenService参数
    61         DefaultTokenServices tokenService = new DefaultTokenServices();
    62         tokenService.setTokenStore(endpoints.getTokenStore());
    63         tokenService.setSupportRefreshToken(true);
    64         tokenService.setClientDetailsService(endpoints.getClientDetailsService());
    65         tokenService.setTokenEnhancer(endpoints.getTokenEnhancer());
    66         tokenService.setAccessTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(30)); //30天
    67        tokenService.setRefreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(50)); //50天
    68         tokenService.setReuseRefreshToken(false);
    69         endpoints.tokenServices(tokenService);
    70         
    71     }
    72 
    73     @Override
    74     public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    75         //允许表单认证
    76         //这里增加拦截器到安全认证链中,实现自定义认证,包括图片验证,短信验证,微信小程序,第三方系统,CAS单点登录
    77         //addTokenEndpointAuthenticationFilter(IntegrationAuthenticationFilter())
    78         //IntegrationAuthenticationFilter 采用 @Component 注入
    79         oauthServer.allowFormAuthenticationForClients()
    80                    .tokenKeyAccess("isAuthenticated()")
    81                    .checkTokenAccess("permitAll()");
    82     }
    83     
    84 }

    (3) ResourceServerConfiguration.java

     1 /**
     2  * OAuth 资源服务器配置
     3  * @author wunaozai
     4  * @date 2018-05-29
     5  */
     6 @Configuration
     7 @EnableResourceServer
     8 public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
     9     
    10     private static final String DEMO_RESOURCE_ID = "order";
    11     
    12     @Override
    13     public void configure(ResourceServerSecurityConfigurer resources) {
    14         resources.resourceId(DEMO_RESOURCE_ID).stateless(true);
    15     }
    16 
    17     @Override
    18     public void configure(HttpSecurity http) throws Exception {
    19         // Since we want the protected resources to be accessible in the UI as well we need
    20         // session creation to be allowed (it's disabled by default in 2.0.6)
    21         http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
    22             .and()
    23             .requestMatchers().anyRequest()
    24             .and()
    25             .anonymous()
    26             .and()
    27 //            .authorizeRequests()
    28 //            .antMatchers("/order/**").authenticated();//配置order访问控制,必须认证过后才可以访问
    29             .authorizeRequests()
    30             .antMatchers("/order/**").hasAuthority("admin_role");//配置访问控制,必须具有admin_role权限才可以访问资源
    31 //            .antMatchers("/order/**").hasAnyRole("admin");
    32     }
    33     
    34 }

    (4) BaseClientDetailService.java

     1 /**
     2  * 自定义客户端认证
     3  * @author wunaozai
     4  * @date 2018-06-20
     5  */
     6 public class BaseClientDetailService implements ClientDetailsService {
     7 
     8     private static final Logger log = LoggerFactory.getLogger(BaseClientDetailService.class);
     9 
    10     @Override
    11     public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
    12         System.out.println(clientId);
    13         BaseClientDetails client = null;
    14         //这里可以改为查询数据库
    15         if("client".equals(clientId)) {
    16             log.info(clientId);
    17             client = new BaseClientDetails();
    18             client.setClientId(clientId);
    19             client.setClientSecret("{noop}123456");
    20             //client.setResourceIds(Arrays.asList("order"));
    21             client.setAuthorizedGrantTypes(Arrays.asList("authorization_code", 
    22                     "client_credentials", "refresh_token", "password", "implicit"));
    23             //不同的client可以通过 一个scope 对应 权限集
    24             client.setScope(Arrays.asList("all", "select"));
    25             client.setAuthorities(AuthorityUtils.createAuthorityList("admin_role"));
    26             client.setAccessTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(1)); //1天
    27             client.setRefreshTokenValiditySeconds((int)TimeUnit.DAYS.toSeconds(1)); //1天
    28             Set<String> uris = new HashSet<>();
    29             uris.add("http://localhost:8080/login");
    30             client.setRegisteredRedirectUri(uris);
    31         }
    32         if(client == null) {
    33             throw new NoSuchClientException("No client width requested id: " + clientId);
    34         }
    35         return client;
    36     }
    37     
    38 }

    (5) BaseUserDetailService.java

     1 /**
     2  * 自定义用户认证Service
     3  * @author wunaozai
     4  * @date 2018-06-19
     5  */
     6 //@Service
     7 public class BaseUserDetailService implements UserDetailsService {
     8 
     9     private static final Logger log = LoggerFactory.getLogger(BaseUserDetailService.class);
    10 
    11     @Override
    12     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    13         log.info(username);
    14         System.out.println(username);
    15         //return new User(username, "{noop}123456", false, false, null);
    16         //User user = null;
    17         SysUserAuthentication user = null; 
    18         if("admin".equals(username)) {
    19             IntegrationAuthentication auth = IntegrationAuthenticationContext.get();
    20             //这里可以通过auth 获取 user 值
    21             //然后根据当前登录方式type 然后创建一个sysuserauthentication 重新设置 username 和 password
    22             //比如使用手机验证码登录的, username就是手机号 password就是6位的验证码{noop}000000
    23             System.out.println(auth);
    24             List<GrantedAuthority> list = AuthorityUtils.createAuthorityList("admin_role"); //所谓的角色,只是增加ROLE_前缀
    25             user = new SysUserAuthentication();
    26             user.setUsername(username);
    27             user.setPassword("{noop}123456");
    28             user.setAuthorities(list);
    29             user.setAccountNonExpired(true);
    30             user.setAccountNonLocked(true);
    31             user.setCredentialsNonExpired(true);
    32             user.setEnabled(true);
    33             
    34             //user = new User(username, "{noop}123456", list);
    35             log.info("---------------------------------------------");
    36             log.info(user.toJSONString());
    37             log.info("---------------------------------------------");
    38             //这里会根据user属性抛出锁定,禁用等异常
    39         }
    40         
    41         return user;//返回UserDetails的实现user不为空,则验证通过
    42     }
    43 }

    (6) SysGrantedAuthority.java

     1 /**
     2  * 授权权限模型
     3  * @author wunaozai
     4  * @date 2018-06-20
     5  */
     6 public class SysGrantedAuthority extends BaseModel implements GrantedAuthority {
     7 
     8     private static final long serialVersionUID = 5698641074914331015L;
     9 
    10     /**
    11      * 权限
    12      */
    13     private String authority;
    14 
    15     /**
    16      * 权限
    17      * @return authority 
    18      */
    19     public String getAuthority() {
    20         return authority;
    21     }
    22 
    23     /**
    24      * 权限
    25      * @param authority 权限
    26      */
    27     public void setAuthority(String authority) {
    28         this.authority = authority;
    29     }
    30     
    31 }

    (7) SysUserAuthentication.java

      1 /**
      2  * 认证用户主体模型
      3  * @author wunaozai
      4  * @date 2018-06-19
      5  */
      6 public class SysUserAuthentication extends BaseModel implements UserDetails {
      7 
      8     private static final long serialVersionUID = 2678080792987564753L;
      9 
     10     /**
     11      * ID号
     12      */
     13     private String uuid;
     14     /**
     15      * 用户名
     16      */
     17     private String username;
     18     /**
     19      * 密码
     20      */
     21     private String password;
     22     /**
     23      * 账户生效
     24      */
     25     private boolean accountNonExpired;
     26     /**
     27      * 账户锁定
     28      */
     29     private boolean accountNonLocked;
     30     /**
     31      * 凭证生效
     32      */
     33     private boolean credentialsNonExpired;
     34     /**
     35      * 激活状态
     36      */
     37     private boolean enabled;
     38     /**
     39      * 权限列表
     40      */
     41     private Collection<GrantedAuthority>  authorities;
     42     /**
     43      * ID号
     44      * @return uuid 
     45      */
     46     public String getUuid() {
     47         return uuid;
     48     }
     49     
     50     /**
     51      * ID号
     52      * @param uuid ID号
     53      */
     54     public void setUuid(String uuid) {
     55         this.uuid = uuid;
     56     }
     57     
     58     /**
     59      * 用户名
     60      * @return username 
     61      */
     62     public String getUsername() {
     63         return username;
     64     }
     65     
     66     /**
     67      * 用户名
     68      * @param username 用户名
     69      */
     70     public void setUsername(String username) {
     71         this.username = username;
     72     }
     73     
     74     /**
     75      * 密码
     76      * @return password 
     77      */
     78     public String getPassword() {
     79         return password;
     80     }
     81     
     82     /**
     83      * 密码
     84      * @param password 密码
     85      */
     86     public void setPassword(String password) {
     87         this.password = password;
     88     }
     89     
     90     /**
     91      * 账户生效
     92      * @return accountNonExpired 
     93      */
     94     public boolean isAccountNonExpired() {
     95         return accountNonExpired;
     96     }
     97     
     98     /**
     99      * 账户生效
    100      * @param accountNonExpired 账户生效
    101      */
    102     public void setAccountNonExpired(boolean accountNonExpired) {
    103         this.accountNonExpired = accountNonExpired;
    104     }
    105     
    106     /**
    107      * 账户锁定
    108      * @return accountNonLocked 
    109      */
    110     public boolean isAccountNonLocked() {
    111         return accountNonLocked;
    112     }
    113     
    114     /**
    115      * 账户锁定
    116      * @param accountNonLocked 账户锁定
    117      */
    118     public void setAccountNonLocked(boolean accountNonLocked) {
    119         this.accountNonLocked = accountNonLocked;
    120     }
    121     
    122     /**
    123      * 凭证生效
    124      * @return credentialsNonExpired 
    125      */
    126     public boolean isCredentialsNonExpired() {
    127         return credentialsNonExpired;
    128     }
    129     
    130     /**
    131      * 凭证生效
    132      * @param credentialsNonExpired 凭证生效
    133      */
    134     public void setCredentialsNonExpired(boolean credentialsNonExpired) {
    135         this.credentialsNonExpired = credentialsNonExpired;
    136     }
    137     
    138     /**
    139      * 激活状态
    140      * @return enabled 
    141      */
    142     public boolean isEnabled() {
    143         return enabled;
    144     }
    145     
    146     /**
    147      * 激活状态
    148      * @param enabled 激活状态
    149      */
    150     public void setEnabled(boolean enabled) {
    151         this.enabled = enabled;
    152     }
    153     
    154     /**
    155      * 权限列表
    156      * @return authorities 
    157      */
    158     public Collection<GrantedAuthority> getAuthorities() {
    159         return authorities;
    160     }
    161     
    162     /**
    163      * 权限列表
    164      * @param authorities 权限列表
    165      */
    166     public void setAuthorities(Collection<GrantedAuthority> authorities) {
    167         this.authorities = authorities;
    168     }
    169     
    170 }

    3.PostMan工具接口测试

    (0) /oauth/token 登录

      这个如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走ClientCredentialsTokenEndpointFilter来保护
      如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的,走basic认证保护

    (1) /oauth/token client_credentials模式

      如代码所示,增加了一个client/123456 的Client账户,里面有client_credentials授权模式
      通过postman请求如下

      获取到access_token后,使用该token请求受保护的资源/order/demo

      如果是错误的access_token的那么会提示invalid_token

      其实像我们这种小公司,小项目,基本上用这个也就可以了,自己的帐号密码,然后接入第三方微信、QQ之类的。哈哈。
    (2) /oauth/token password模式

      这种方式比上一种方式更适合我们公司使用,因为我们公司对外提供接入方式,基本是提供给我们的代理商,而我们更希望帐号和服务都由我们提供,基本目前几年内不会提供给代理商第三方登录,也没有必要。所以这里的帐号密码都是由我们服务器统一管理。
    (3) /oauth/token code 模式
      /oauth/authorize 
      这个比较复杂。我就一步一步的说明。

      首先要通过/oauth/token进行登录,可以使用以上(0)(2)方式登录,注意登录是scope的填写。登录成功后,得到access_token.然后请求/oauth/authorize地址,注意参数redirect_uri是要跳转到的第三方地址上。

      一般通过GET方式访问,如果合法的话(合法,判断access_token和对应的scope)那么浏览器会跳转到redirect_uri指定的地址。

      访问成功后,会返回一个code值。第三方厂商就可以根据这个code去获取用户的access_token然后访问受限资源。

      一个code只能使用一次,如果多次使用那么会报错

    1 {
    2     "error": "invalid_grant",
    3     "error_description": "Invalid authorization code: 55ffrh"
    4 }

      注意这里的redirect_uri根据服务器BaseClientDetailService中配置的uri是一致的,否则不通过。

      这种方式是OAuth最好的一种方式,只是基于公司,项目的实际考虑,这种方式,比较繁琐,目前是不会用到的。

      刚才想了一下,好像第三方获取到的access_token就是用户登录后的access_token,觉得不对,想了想,应该是用户要通过scope对权限进行限制。而这里的scope会对应到资源权限部分。

    (4) implicit模式 略,基本参考标准OAuth2.0就可以啦

    (5) check_token 检查token是否合法

    (6) refresh_token 刷新token

    调用时access_token,refresh_token均未过期
    access_token会变,而且expires延长,refresh_token根据设定的过期时间,没有失效则不变
    {"access_token":"eb45f1d4-54a5-4e23-bf12-31d8d91a902f","token_type":"bearer","refresh_token":"efa96270-18a1-432c-b9e6-77725c0dabea","expires_in":1199,"scope":"all"}
    
    调用时access_token过期,refresh_token未过期
    access_token会变,而且expires延长,refresh_token根据设定的过期时间,没有失效则不变
    {"access_token":"a78999d6-614a-45fe-be58-d5e0b6451bdb","token_type":"bearer","refresh_token":"bb2a0165-769d-43b0-a9a5-1331012ede1f","expires_in":119,"scope":"all"}
    
    调用时refresh_token过期
    {"error":"invalid_token","error_description":"Invalid refresh token (expired): 95844d87-f06e-4a4e-b76c-f16c5329e287"}
    

      

      关于OAuth里面的知识还有很多细节没有理解透,随着项目的深入,慢慢了解吧。

    -----------------2019-06-11 更新-------------------------

    评论区:

      问:请教一下根据用户角色不同访问不同请求,这个怎么搞呢?

      答:在 BaseUserDetailService.java 里面 第24、28行,表示对当前登录的账户增加一个角色,角色名称“admin_role”

    1 List<GrantedAuthority> list = AuthorityUtils.createAuthorityList("admin_role");
    2 user.setAuthorities(list);

      方式1:然后针对URL请求,设置对应的可以访问的权限,在 ResourceServerConfiguration.java 第31行

    1 .antMatchers("/order/**").hasAuthority("admin_role");//配置访问控制,必须具有admin_role权限才可以访问资源

      方式2:上面这种通过配置的方式,有时不是很灵活,一般我是通过注解方式来设置URL请求所需要的权限,下面这个代码就表示在这整个控制器内的所有请求都是需要“admin_role”权限。

    1 @RestController
    2 @RequestMapping(value="/order/demo")
    3 @PreAuthorize("hasAnyAuthority('admin_role')")
    4 public class CustBomController {
    5 }

      @PreAuthorize这个注解,除了类注解,还可以对方法体进行注解,注解还可以通过 and or 进行多个角色权限进行控制。具体你查询网上资料。

    参考资料:

      https://github.com/lexburner/oauth2-demo

      http://blog.didispace.com/spring-security-oauth2-xjf-1/ 

      https://www.cnblogs.com/cjsblog/p/9152455.html

      https://segmentfault.com/a/1190000014371789 (多种认证方式)

  • 相关阅读:
    小项目心得交流
    自己写的web标准教程,帮你走进web标准设计的世界——第三讲(html终结篇)
    css之清除区域
    面向对象大作业(自主选题)
    关于vue在hash模式偶发不能后退的处理
    flex布局设置单个子元素靠右
    css 选择器
    Git常用命令及方法大全
    解决微信sdk之uploadImage上传多张图片时循环提示“上传中”
    grid 布局
  • 原文地址:https://www.cnblogs.com/wunaozai/p/9205550.html
Copyright © 2020-2023  润新知