认证方式

  1. 客户端授权(Client Credentials Grant)

    1
    2
    3
    
    POST /oauth/token?grant_type=client_credentials HTTP/1.1
    Host: server.example.com
    Authorization: Basic Y2xpZW50X2F1dGhfbW9kZToxMjM0NTY=
    • 在mysql中建立表:oauth_client_details
    • 请求头Authorization,key=Authorization;value=Basic+空格+Base64(username:password),Basic后面的信息由username:password内的字符Base64加密而成。
    • username和password分别为oauth_client_details表中的client_id和client_secret,也就是客户端模式下的标识客户端的凭证(用以区别是哪种受信任的客户端),对应OAuth2映射为ClientDetails对象。
  2. 密码授权

    1
    2
    3
    4
    5
    
    POST /token HTTP/1.1
    Host: server.example.com
    Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
    Content-Type: application/x-www-form-urlencoded
    grant_type=password&username=johndoe&password=A3ddj3w
    • 请求头Authorization,key=Authorization;value=Basic+空格+Base64(username:password),Basic后面的信息由username:password内的字符Base64加密而成。
    • username和password依旧为oauth_client_details表中的client_id和client_secret,也就是客户端模式下的标识客户端的凭证(用以区别是哪种受信任的客户端),对应OAuth2映射为DetailDetails对象。
    • 与客户端模式相比多参数:username和password,在于密码模式下,Spring-Security-Oauth2中,有个叫做UserDetails的对象。
    • 在客户端模式下,需要对ClientDetails对象进行认证;而在密码模式下,则既需要对ClientDetails对象认证,也需要对UserDetails对象认证。

认证配置

认证管理信息的配置主要是针对ClientDetails和UserDetails对象的检查,客户端模式针对ClientDetails检查,而密码模式则先检查ClientDetails后检查UserDetails对象。 认证授权配置如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@Configuration
@EnableAuthorizationServer//开启配置 OAuth 2.0 认证授权服务
public class AuthAuthorizeConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    DataSource dataSource;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private CustomUserDetailsService userDetailsService;
    /**
     * 配置 oauth_client_details【client_id和client_secret等】信息的认证
     *【检查ClientDetails的合法性】服务
     * 设置 认证信息的来源:数据库 (可选项:数据库和内存,使用内存一般用来作测试)
     * 自动注入:ClientDetailsService的实现类 JdbcClientDetailsService
     *(检查 ClientDetails 对象)
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
      throws Exception {
        clients.jdbc(dataSource);
    }


    /**
     * 密码模式下配置认证管理器 AuthenticationManager,并且设置 AccessToken
     * 的存储介质tokenStore,如果不设置,则会默认使用内存当做存储介质。
     * 而该AuthenticationManager将会注入 2个Bean对象用以检查(认证)
     * 1、ClientDetailsService的实现类 JdbcClientDetailsService
     * (检查 ClientDetails 对象)
     * 2、UserDetailsService的实现类 CustomUserDetailsService 
     * (检查 UserDetails 对象)
     * 
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints)
      throws Exception {
        endpoints.authenticationManager(authenticationManager)
          .tokenStore(tokenStore)
          .userDetailsService(userDetailsService);
    }

    /**
     *  配置:安全检查流程
     *  默认过滤器:BasicAuthenticationFilter
     *  1、oauth_client_details表中clientSecret字段加密
     * 【ClientDetails属性secret】
     *  2、CheckEndpoint类的接口 oauth/check_token 无需经过过滤器过滤,
     *  默认值:denyAll()
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();//允许客户表单认证
        //设置oauth_client_details中的密码编码器
        security.passwordEncoder(new BCryptPasswordEncoder());
        //对于CheckEndpoint控制器[框架自带的校验]的/oauth/check端点
        //允许所有客户端发送器请求而不会被Spring-security拦截
        security.checkTokenAccess("permitAll()");
    }
}

AccessToken

客户端认证

请求的报文信息:

1
2
3
4
5
POST /oauth2-server/oauth/token?grant_type=client_credentials HTTP/1.1
Host: server.example.com
Authorization: Basic Y2xpZW50X2F1dGhfbW9kZToxMjM0NTY=
Cache-Control: no-cache
Content-Type: multipart/form-data

返回结果:

1
2
3
4
5
6
{
    "access_token": "afef641c-62de-4f5d-a5b8-7864ac2b7127",
    "token_type": "bearer",
    "expires_in": 3463,
    "scope": "APP"
}

用户名密码认证

请求的报文信息:

1
2
3
4
5
POST /oauth2-server/oauth/token?username=admin&password=e10adc3949ba59abbe56e057f20f883e&grant_type=password&client_id=webapp&client_secret=123456 HTTP/1.1
Host: server.example.com
Authorization: Basic cGFzc3dvcmRfYXV0aF9tb2RlOjEyMzQ1Ng==
Cache-Control: no-cache
Content-Type: multipart/form-data

返回结果:

1
2
3
4
5
6
7
{
    "access_token": "a83ba33f-9f1a-4f9a-ba65-99e7fc905ba2",
    "token_type": "bearer",
    "refresh_token": "89f724d6-8553-4838-b4ff-7f6c8fb4d88b",
    "expires_in": 3378,
    "scope": "APP"
}

两者区别

  1. 客户端授权返回结果比密码模式返回结果少了refresh_token,因为客户模式不支持refresh_token认证。
  2. 客户端授权(client_credentials)是受信任的认证模式,即可以设置为永久性的AccessToken,而不需要刷新重新获取AccessToken。

获取AccessToken

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
 *  认证登录接口(获取AccessToken)
 */
@RestController
@RequestMapping("/api/oauth2")
public class Oauth2Controller {

    private static final Logger log = LoggerFactory.getLogger(Oauth2Controller.class);

    /**
     * OAuth2的用户名密码授权模式
     */
    @RequestMapping(value = "/passwordMode",method = RequestMethod.POST)
    public Object accessToken(
      @RequestParam(value = "client_id") String clientId,
      @RequestParam(value = "client_secret") String clientSecret,
      @RequestParam(value = "grant_type") String grantType,
      @RequestParam(value = "username") String username,
      @RequestParam(value = "password") String password){
        //创建一个包含需要请求的资源实体以及认证信息集合的对象
        ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
        //认证授权的服务器地址(http://localhost:${server.port}/oauth/token)
        details.setAccessTokenUri("your_access_token_uri");
      
        details.setScope(Arrays.asList("read", "write"));
        details.setClientId(clientId);
        details.setClientSecret(clientSecret);
        details.setUsername(username);
        //BCrypt加密
        details.setPassword(new BCryptPasswordEncoder()
          .encode(password));

        ResourceOwnerPasswordAccessTokenProvider provider = new ResourceOwnerPasswordAccessTokenProvider();
        OAuth2AccessToken accessToken = null;
        try {
            //获取AccessToken
            accessToken = provider.obtainAccessToken(details, new DefaultAccessTokenRequest());
        } catch (NullPointerException e) {
            log.error("授权失败原因:{}", e.getMessage());
            return "用户不存在";
        }catch (Exception e){
            log.error("授权失败原因:{}", e.getMessage());
            return "创建token失败";
        }
        return accessToken;
    }

    /**
     * Oauth2的受信任的客户端授权模式
     */
    @RequestMapping(value = "/clientMode",method = RequestMethod.POST)
    public Object getToken(
      @RequestParam(value = "client_id") String clientId,
      @RequestParam(value = "client_secret") String clientSecret,
      @RequestParam(value = "grant_type") String grantType){
        //创建一个包含需要请求的资源实体以及认证信息集合的对象
        ClientCredentialsResourceDetails clientCredentials = new ClientCredentialsResourceDetails();
        //认证授权的服务器地址(http://localhost:${server.port}/oauth/token)
        clientCredentials.setAccessTokenUri("your_access_token_uri");
        //下面都是认证信息:所拥有的权限,认证的客户端
        clientCredentials.setScope(Arrays.asList("APP", "web"));
        clientCredentials.setClientId(clientId);
        clientCredentials.setClientSecret(clientSecret);
        clientCredentials.setGrantType(grantType);
        ClientCredentialsAccessTokenProvider provider = new ClientCredentialsAccessTokenProvider();
        OAuth2AccessToken accessToken = null;
        try {
            accessToken = provider.obtainAccessToken(clientCredentials,new DefaultAccessTokenRequest());
        } catch (Exception e) {
            e.printStackTrace();
            return "获取AccessToken失败";
        }
        return accessToken;
    }

}

AccessToken有效性

拦截器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * 对AccessToken进行检测,当出现AccessToken失效或者非法时,将直接返回401,未授权错误
 */
public class Oauth2Interceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        String accessToken = request.getParameter("access_token");
        OAuth2AccessToken oauth2AccessToken = Oauth2Utils.checkTokenInOauth2Client(accessToken);
        if (oauth2AccessToken==null){//非法的Token值
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            ResponseUtils.responseData(response,"非法的Token!");
            return false;
        }else if (oauth2AccessToken.isExpired()){//token失效
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            ResponseUtils.responseData(response,"Token失效,请重新登录!");
            return false;
        }
         return true;
    }
}

注册拦截器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Configuration
@EnableWebMvc //开启spring mvc的相关默认配置
public class InterceptorRegisterConfiguration extends WebMvcConfigurerAdapter {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new Oauth2Interceptor())
        .excludePathPatterns("/api/oauth2/**");//添加Oauth2Interceptor,除了/api/oauth2/**下的接口都需要进行 AccessToken 的校验
    }
}

根据token获取OAuth2AccessToken与OAuth2Authentication

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/oauth")
public class TokenController {

    /**
     * 覆盖了 spring-security-oauth2 内部的 endpoint oauth2/check_token
     * spring-security-oauth2 内部原有的该控制器 CheckTokenEndpoint,返回值,不符合自身业务要求,故覆盖之。
     */
    @GetMapping("/check_token")
    public OAuth2AccessToken getToken(
      @RequestParam(value = "token") String token){
        OAuth2AccessToken oAuth2AccessToken = Oauth2Utils.checkTokenInOauth2Server(token);
        return oAuth2AccessToken;
    }

    /**
     * 获取当前token对应的用户主体的凭证信息(认证对象)
     */
    @GetMapping("/getAuth")
    public OAuth2Authentication getAuth(@RequestParam(value = "token") String token){
        OAuth2Authentication oAuth2Authentication = Oauth2Utils.getAuthenticationInOauth2Server(token);
        return oAuth2Authentication;
    }

工具类

Oauth2Utils

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Oauth2Utils {
    /**
     * oauth2 认证服务器直接处理校验请求的逻辑
     * @param accessToken
     * @return
     */
    public static OAuth2AccessToken  checkTokenInOauth2Server(String accessToken){
        TokenStore tokenStore = (TokenStore) ApplicationSupport.getBean("tokenStore");
        OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(accessToken);
        return oAuth2AccessToken;
    }

    /**
     * oauth2 认证服务器直接处理校验请求的逻辑
     * @param accessToken
     * @return
     */
    public static OAuth2Authentication  getAuthenticationInOauth2Server(String accessToken){
        TokenStore tokenStore = (TokenStore) ApplicationSupport.getBean("tokenStore");
        OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken);
        return oAuth2Authentication;
    }
}

ApplicationSupport

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 获取Spring容器管理的Bean对象,应用中配置参数
**/
@Component
public class ApplicationSupport implements DisposableBean, ApplicationContextAware {

    private static ApplicationContext applicationContext;
    // 获取配置文件参数值
    public static String getParamVal(String paramKey){
        return applicationContext.getEnvironment().getProperty(paramKey);
    }

    // 获取bean对象
    public static Object getBean(String name) {
        Assert.hasText(name);
        return applicationContext.getBean(name);
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
   public void destroy() throws Exception {
        applicationContext = null;
    }

}

配置文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
security:
    basic:
        enabled: false # 是否开启基本的鉴权,默认为true。 true:所有的接口默认都需要被验证,将导致 拦截器[对于 excludePathPatterns()方法失效]
server:
  context-path: /oauth2-client
  port: 8061
---
spring:
  application:
      name: oauth2-client
  datasource: #数据源的配置
    url: jdbc:mysql://127.0.0.1:3306/oauth2?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: 123456
  jpa: #jpa的支持:hibernate的相关配置
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    database: MYSQL
    openInView: true
    show_sql: true
    generate-ddl: true #(false)
    hibernate:
        ddl-auto: update #(none)

oauth: #oauth2-server认证授权服务器的url配置,在获取AccessToken以及检测AccessToken中会用到
  token: http://127.0.0.1:8050/oauth2-server/oauth/token
  check_token: http://localhost:8050/oauth2-server/oauth/check_token #检查AccessToken有效性的url(认证授权服务器的url地址),获取 AccessToken 对象。

本文为学习记录,因能力有限,如有错误请赐教……如需转载,请注明出处!