OAuth2.0用户名,密码登录解析

Stella981
• 阅读 547

OAuth2的原理应该从这张图说起

OAuth2.0用户名,密码登录解析

下面是相关的类图

OAuth2.0用户名,密码登录解析

OAuth2.0用户名,密码登录解析

首先我们从请求认证开始http://127.0.0.1:63739/oauth/token?grant\_type=password&client\_id=system&client\_secret=system&scope=app&username=admin&password=admin

返回值为{
    "access_token": "a18a9359-cfc0-4d29-a16d-7ea75388d0e9",
    "token_type": "bearer",
    "refresh_token": "21a20eb7-69dd-499d-bc65-36343bc4cc88",
    "expires_in": 28799,
    "scope": "app"
}

进入oauth2源码TokenEndpoint,我们可以看到(加了注释)

@RequestMapping( value = {"/oauth/token"}, method = {RequestMethod.POST} ) public ResponseEntity postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { if(!(principal instanceof Authentication)) { throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter."); } else { String clientId = this.getClientId(principal);         //从数据库表oauth_client_details,通过clientId获取clientDetails,clientDetails是一个序列化类 ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);         //产生一个带参数请求的Request TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient); if(clientId != null && !clientId.equals("") && !clientId.equals(tokenRequest.getClientId())) { throw new InvalidClientException("Given client ID does not match authenticated client"); } else { if(authenticatedClient != null) {                 //验证范围 this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient); }

        if(!StringUtils.hasText(tokenRequest.getGrantType())) {
            throw new InvalidRequestException("Missing grant type");
        } else if(tokenRequest.getGrantType().equals("implicit")) {
            throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
        } else {
            if(this.isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) {
                this.logger.debug("Clearing scope of incoming token request");
                tokenRequest.setScope(Collections.emptySet());
            }

            if(this.isRefreshTokenRequest(parameters)) {
                tokenRequest.setScope(OAuth2Utils.parseParameterList((String)parameters.get("scope")));
            }

                //对这种登录方式进行授权,产生一个通过token,OAuth2AccessToken是一个序列化类 OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest); if(token == null) { throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType()); } else { return this.getResponse(token); } } } } }

其中ClientDetailsService是一个接口,它决定了从哪里获取clientDetails,它有2个实现类,一个是从内存中InMemoryClientDetailsService

,一个是从数据库中JdbcClientDetailsService.

.我们主要讲从数据库中获取clientDetails.

public JdbcClientDetailsService(DataSource dataSource) { this.updateClientDetailsSql = DEFAULT_UPDATE_STATEMENT; this.updateClientSecretSql = "update oauth_client_details set client_secret = ? where client_id = ?"; this.insertClientDetailsSql = "insert into oauth_client_details (client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove, client_id) values (?,?,?,?,?,?,?,?,?,?,?)"; this.selectClientDetailsSql = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?"; this.passwordEncoder = NoOpPasswordEncoder.getInstance(); Assert.notNull(dataSource, "DataSource required"); this.jdbcTemplate = new JdbcTemplate(dataSource); this.listFactory = new DefaultJdbcListFactory(new NamedParameterJdbcTemplate(this.jdbcTemplate)); }

public void setPasswordEncoder(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; }

public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException { try { ClientDetails details = (ClientDetails)this.jdbcTemplate.queryForObject(this.selectClientDetailsSql, new JdbcClientDetailsService.ClientDetailsRowMapper(), new Object[]{clientId}); return details; } catch (EmptyResultDataAccessException var4) { throw new NoSuchClientException("No client with requested id: " + clientId); } }

数据库中数据如下

OAuth2.0用户名,密码登录解析

而我们需要在

@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter

中配置使用数据库,而不是在内存中获取clientDetails

@Autowired private DataSource dataSource;

使用Resource的yml文件中dataSource配置(以下使用的是mysql 8的配置)

spring:    datasource:     driver-class-name: com.mysql.cj.jdbc.Driver     url: jdbc:mysql://XXX.XXX.XXX.XXX:3306/cloud_oauth?useSSL=FALSE&serverTimezone=UTC     username: root     password: xxxxxx     type: com.alibaba.druid.pool.DruidDataSource     filters: stat     maxActive: 20     initialSize: 1     maxWait: 60000     minIdle: 1     timeBetweenEvictionRunsMillis: 60000     minEvictableIdleTimeMillis: 300000     validationQuery: select 'x'     testWhileIdle: true     testOnBorrow: false     testOnReturn: false     poolPreparedStatements: true     maxOpenPreparedStatements: 20

以下是使用dataSource来配置jdbc,请注意注释掉的是内存配置,如果使用内存配置,将不会使用数据库配置.

@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // clients.inMemory().withClient("system").secret("system") // .authorizedGrantTypes("password", "authorization_code", "refresh_token").scopes("app") // .accessTokenValiditySeconds(3600);

  clients.jdbc(dataSource);

}

另外一个重点的地方就是授权登录OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);TokenGranter也是一个接口,有一个抽象类AbstractTokenGranter实现该接口.授权方法

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) { if(!this.grantType.equals(grantType)) { return null; } else { String clientId = tokenRequest.getClientId(); ClientDetails client = this.clientDetailsService.loadClientByClientId(clientId); this.validateGrantType(grantType, client); if(this.logger.isDebugEnabled()) { this.logger.debug("Getting access token for: " + clientId); }

    return this.getAccessToken(client, tokenRequest);
}

}

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) { return this.tokenServices.createAccessToken(this.getOAuth2Authentication(client, tokenRequest)); }

getAccessToken里有一个tokenServices.createAccessToken.tokenServices的定义为private final AuthorizationServerTokenServices tokenServices;AuthorizationServerTokenServices也是一个接口.实现类只有1个

public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices, ConsumerTokenServices, InitializingBean

而且这个实现类同时实现了很多个接口.

@Transactional public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {     //如果不是第一次登陆,从tokenStore取出通过token;如果是第一次登陆为null OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication); OAuth2RefreshToken refreshToken = null; if(existingAccessToken != null) { if(!existingAccessToken.isExpired()) {             //如果不是第一次登陆未过期,将token重新存入tokenStore this.tokenStore.storeAccessToken(existingAccessToken, authentication); return existingAccessToken; }         //如果已经过期,移除token if(existingAccessToken.getRefreshToken() != null) { refreshToken = existingAccessToken.getRefreshToken(); this.tokenStore.removeRefreshToken(refreshToken); }

    this.tokenStore.removeAccessToken(existingAccessToken);
}

    //如果是第一次登陆,先创建RefreshToken if(refreshToken == null) { refreshToken = this.createRefreshToken(authentication); } else if(refreshToken instanceof ExpiringOAuth2RefreshToken) { ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken; if(System.currentTimeMillis() > expiring.getExpiration().getTime()) { refreshToken = this.createRefreshToken(authentication); } }     //创建token OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);     //将token存入tokenStore this.tokenStore.storeAccessToken(accessToken, authentication); refreshToken = accessToken.getRefreshToken(); if(refreshToken != null) {         //将refreshToken存入tokenStore this.tokenStore.storeRefreshToken(refreshToken, authentication); }

return accessToken;

}

创建一个UUID的RefreshToken

private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) { if(!this.isSupportRefreshToken(authentication.getOAuth2Request())) { return null; } else { int validitySeconds = this.getRefreshTokenValiditySeconds(authentication.getOAuth2Request()); String value = UUID.randomUUID().toString(); return (OAuth2RefreshToken)(validitySeconds > 0?new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis() + (long)validitySeconds * 1000L)):new DefaultOAuth2RefreshToken(value)); } }

创建一个UUID的token

private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) { DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());     //校验时间 int validitySeconds = this.getAccessTokenValiditySeconds(authentication.getOAuth2Request()); if(validitySeconds > 0) { token.setExpiration(new Date(System.currentTimeMillis() + (long)validitySeconds * 1000L)); }

token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return (OAuth2AccessToken)(this.accessTokenEnhancer != null?this.accessTokenEnhancer.enhance(token, authentication):token);

}

其中TokenStore是一个接口,有5个实现类InMemoryTokenStore内存存储,JdbcTokenStore数据库存储,JwkTokenStore,JwtTokenStore,RedisTokenStore Redis存储,我们主要讲Redis存储.

redis存储token的代码

public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { byte[] serializedAccessToken = this.serialize((Object)token); byte[] serializedAuth = this.serialize((Object)authentication); byte[] accessKey = this.serializeKey("access:" + token.getValue()); byte[] authKey = this.serializeKey("auth:" + token.getValue()); byte[] authToAccessKey = this.serializeKey("auth_to_access:" + this.authenticationKeyGenerator.extractKey(authentication)); byte[] approvalKey = this.serializeKey("uname_to_access:" + getApprovalKey(authentication)); byte[] clientId = this.serializeKey("client_id_to_access:" + authentication.getOAuth2Request().getClientId()); RedisConnection conn = this.getConnection();

try {
    conn.openPipeline();
    if(springDataRedis\_2\_0) {
        try {
            this.redisConnectionSet\_2\_0.invoke(conn, new Object\[\]{accessKey, serializedAccessToken});
            this.redisConnectionSet\_2\_0.invoke(conn, new Object\[\]{authKey, serializedAuth});
            this.redisConnectionSet\_2\_0.invoke(conn, new Object\[\]{authToAccessKey, serializedAccessToken});
        } catch (Exception var24) {
            throw new RuntimeException(var24);
        }
    } else {
        conn.set(accessKey, serializedAccessToken);
        conn.set(authKey, serializedAuth);
        conn.set(authToAccessKey, serializedAccessToken);
    }

    if(!authentication.isClientOnly()) {
        conn.rPush(approvalKey, new byte\[\]\[\]{serializedAccessToken});
    }

    conn.rPush(clientId, new byte\[\]\[\]{serializedAccessToken});
    if(token.getExpiration() != null) {
        int seconds = token.getExpiresIn();
        conn.expire(accessKey, (long)seconds);
        conn.expire(authKey, (long)seconds);
        conn.expire(authToAccessKey, (long)seconds);
        conn.expire(clientId, (long)seconds);
        conn.expire(approvalKey, (long)seconds);
    }

    OAuth2RefreshToken refreshToken = token.getRefreshToken();
    if(refreshToken != null && refreshToken.getValue() != null) {
        byte\[\] refresh = this.serialize(token.getRefreshToken().getValue());
        byte\[\] auth = this.serialize(token.getValue());
        byte\[\] refreshToAccessKey = this.serializeKey("refresh\_to\_access:" + token.getRefreshToken().getValue());
        byte\[\] accessToRefreshKey = this.serializeKey("access\_to\_refresh:" + token.getValue());
        if(springDataRedis\_2\_0) {
            try {
                this.redisConnectionSet\_2\_0.invoke(conn, new Object\[\]{refreshToAccessKey, auth});
                this.redisConnectionSet\_2\_0.invoke(conn, new Object\[\]{accessToRefreshKey, refresh});
            } catch (Exception var23) {
                throw new RuntimeException(var23);
            }
        } else {
            conn.set(refreshToAccessKey, auth);
            conn.set(accessToRefreshKey, refresh);
        }

        if(refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken)refreshToken;
            Date expiration = expiringRefreshToken.getExpiration();
            if(expiration != null) {
                int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L).intValue();
                conn.expire(refreshToAccessKey, (long)seconds);
                conn.expire(accessToRefreshKey, (long)seconds);
            }
        }
    }

    conn.closePipeline();
} finally {
    conn.close();
}

}

因此,我们在

@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter

中,需要实例化接口TokenStore为RedisTokenStore.

@Autowired private RedisConnectionFactory redisConnectionFactory;

@Autowired private AuthenticationManager authenticationManager;

@Bean public TokenStore tokenStore() { return new RedisTokenStore(redisConnectionFactory); }

并且具体实现

@Autowired private RedisAuthorizationCodeServices redisAuthorizationCodeServices;

@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(this.authenticationManager); endpoints.tokenStore(tokenStore()); endpoints.authorizationCodeServices(redisAuthorizationCodeServices); }

以上就是把authenticationManager,tokenStore(),redisAuthorizationCodeServices给配置到endpoints中.

@Service public class RedisAuthorizationCodeServices extends RandomValueAuthorizationCodeServices {

@Autowired private RedisTemplate<Object, Object> redisTemplate;

/** * 存储code到redis,并设置过期时间,10分钟
* value为OAuth2Authentication序列化后的字节
* 因为OAuth2Authentication没有无参构造函数
* redisTemplate.opsForValue().set(key, value, timeout, unit); * 这种方式直接存储的话,redisTemplate.opsForValue().get(key)的时候有些问题, * 所以这里采用最底层的方式存储,get的时候也用最底层的方式获取 */ @Override protected void store(String code, OAuth2Authentication authentication) { redisTemplate.execute(new RedisCallback() {

     @Override
     public Long doInRedis(RedisConnection connection) throws DataAccessException {
        connection.set(codeKey(code).getBytes(), SerializationUtils.serialize(authentication),
              Expiration.from(10, TimeUnit.MINUTES), SetOption.UPSERT);
        return 1L;
     }
  });

}

@Override protected OAuth2Authentication remove(final String code) { OAuth2Authentication oAuth2Authentication = redisTemplate.execute(new RedisCallback() {

     @Override
     public OAuth2Authentication doInRedis(RedisConnection connection) throws DataAccessException {
        byte\[\] keyByte = codeKey(code).getBytes();
        byte\[\] valueByte = connection.get(keyByte);

        if (valueByte != null) {
           connection.del(keyByte);
           return SerializationUtils.deserialize(valueByte);
        }

        return null;
     }
  });

  return oAuth2Authentication;

}

/** * 拼装redis中key的前缀 * * @param code * @return */ private String codeKey(String code) { return "oauth2:codes:" + code; } }

OAuth2.0用户名,密码登录解析

我们可以看到redis里大概是这个样子.

最后我们可以用refreshToken来刷新token

http://127.0.0.1:51451/oauth/token?grant\_type=refresh\_token&client\_id=system&client\_secret=system&scope=app&refresh\_token=845d549c-6e73-4bdc-a30d-6991f47353f9

返回{
    "access_token": "923c25aa-71f1-4dbf-9e48-46543d8a8048",
    "token_type": "bearer",
    "refresh_token": "845d549c-6e73-4bdc-a30d-6991f47353f9",
    "expires_in": 28799,
    "scope": "app"
}

这样过期时间就满了

另外我们要实现一个

public interface UserDetailsService { UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException; }

的接口.UserDetails是一个继承了Serializable的接口.

@Service public class UserDetailServiceImpl implements UserDetailsService

我们用一个类来实现UserDetailsService接口

@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 为了支持多类型登录,这里username后面拼装上登录类型,如username|type String[] params = username.split("\\|"); username = params[0];// 真正的用户名     //数据库查询,LoginAppUser是一个实现了UserDetails接口的类 LoginAppUser loginAppUser = userClient.findByUsername(username); if (loginAppUser == null) { throw new AuthenticationCredentialsNotFoundException("用户不存在"); } else if (!loginAppUser.isEnabled()) { throw new DisabledException("用户已作废"); }

return loginAppUser;

}

最后是在Spring Security的安全配置中,对整个Web进行配置

@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter

@Autowired public UserDetailsService userDetailsService;

@Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); }

@Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {    //auth.inMemoryAuthentication().withUser("user").password("password").roles("USER").and().withUser("admin")    //     .password("password").roles("USER", "ADMIN"); auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); }

上面的注释同样是内存注释,而我们是使用数据库来校验用户名,密码.

另外如果配置了FastJson为Web的Json解析器的话,Json的日期格式需要作出调整,否则在Feign调用user-center时会报日期无法解析的错误,OAuth中心和User中心都要做如下设置

@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter(); FastJsonConfig config = new FastJsonConfig(); config.setCharset(Charset.forName("UTF-8")); config.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); // config.setSerializerFeatures(SerializerFeature.WriteMapNullValue); fastJsonConverter.setFastJsonConfig(config); List list = new ArrayList<>(); list.add(MediaType.APPLICATION_JSON_UTF8); fastJsonConverter.setSupportedMediaTypes(list); converters.add(fastJsonConverter); } }

而返回的token格式也会有所变化

{
  "additionalInformation": {},
  "expiration": "2019-05-28T00:22:36.065+0800",
  "expired": false,
  "expiresIn": 28799,
  "refreshToken": {
    "expiration": "2019-06-26T16:22:36.053+0800",
    "value": "b535f2bc-29ce-493b-b562-92271594880a"
  },
  "scope": [
    "app"
  ],
  "tokenType": "bearer",
  "value": "374f96bd-dd6f-4382-a92f-ee417f81b850"
}

点赞
收藏
评论区
推荐文章
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Stella981 Stella981
3年前
2021 最顶级 React 组件库推荐
点上方蓝字关注公众号「前端从进阶到入院」作者丨MaxRozen译者丨王强策划丨小智AntDesign!(https://oscimg.oschina.net/oscnet/a85c35f23bd04e5da6a1e5e68a24119b.png)项目链接:AntDesignh
Stella981 Stella981
3年前
Discuz X3.2源码解析 discuz_application类(转自百度)
1.discuz\_application在/source/class/discuz/discuz\_application.php中。!DiscuzX3.2源码解析discuz_application类(https://oscimg.oschina.net/oscnet/99b35d79caf70b7c74ad0838d6
Stella981 Stella981
3年前
SpringBoot使用@ServerEndpoint无法依赖注入问题解决(WebSocket)
!(https://oscimg.oschina.net/oscnet/up8420801906305684903bceca6a85673cb00.png)!(https://oscimg.oschina.net/oscnet/upf7112141748204868f0368c51f4ea919103.png)如上两图所示,在WebSoc
可莉 可莉
3年前
2021 最顶级 React 组件库推荐
点上方蓝字关注公众号「前端从进阶到入院」作者丨MaxRozen译者丨王强策划丨小智AntDesign!(https://oscimg.oschina.net/oscnet/a85c35f23bd04e5da6a1e5e68a24119b.png)项目链接:AntDesignh
Wesley13 Wesley13
3年前
IJCAI2020 图相关论文集
↑公众号关注“GraphAI”专注于图机器学习IJCAI2020图相关论文集!(https://oscimg.oschina.net/oscnet/538c1eb7d78b304723ca074c35b6cc896f2.png)
Stella981 Stella981
3年前
SAP成都研究院DevOps那些事
!00logo.png(https://oscimg.oschina.net/oscnet/upc4ad68e1489dd93a27450e019a05f7e5.png)!01盗图.png(https://oscimg.oschina.net/oscnet/up63b04cf78c14f773e5c3511cf204d9a1.png)
Stella981 Stella981
3年前
Nginx快速安装
登录nginx官网,点击download!(https://oscimg.oschina.net/oscnet/cbbf95a1f35af2c4d24f0508229bf20e513.png)点击稳定版本!(https://oscimg.oschina.net/oscnet/9b1a9eb422fec4d9c4be2218261418bc
Easter79 Easter79
3年前
SpringMVC的工作原理图
SpringMVC的工作原理图:!(https://oscimg.oschina.net/oscnet/7ec9d18341c77418d09131858910f45ec97.jpg)SpringMVC流程1、 用户发送请求至前端控制器DispatcherServlet。2、 DispatcherServlet收到请求调用Ha
Easter79 Easter79
3年前
SpringBoot使用@ServerEndpoint无法依赖注入问题解决(WebSocket)
!(https://oscimg.oschina.net/oscnet/up8420801906305684903bceca6a85673cb00.png)!(https://oscimg.oschina.net/oscnet/upf7112141748204868f0368c51f4ea919103.png)如上两图所示,在WebSoc