某安某大型国企项目Oauth2实践(二)

前言

token设计相关问题
登录功能相关扩展和实现
Oauth2登录流程源码分析




前言


上篇主要介绍了Oauth2和spring security 的基本概念。本篇接着讲登录过程。谈到登录必然要考虑的问题:

  1.  用户信息怎么存,存什么,存在哪,

  2. 如何保证用户信息的安全,

  3. 如何解决单点登录问题

  4. token盗用问题

  5. 如何防止短信验证码频繁刷新或被劫取

  6. 如何支持多种登录方式或多端登录

  7. 如何处理token过期,刷新问题

  8. token在异步场景中如何传递

  9. 定时任务场景token如何使用

  10. 如何解决登录服务单点故障问题

  11. 第三方服务如何接入



某安某大型国企项目Oauth2实践(二)


1.相关问题解答

1.1 jwt 和token的区别

       token: 服务器验证完用户名和密码后会生成一个签名后的token,一般是随机uuid.

       jwt: JWT是json web token缩写。它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证。 安全性比token要高。

1.2 单点登录问题

采用了jwt 方案,单点登录问题自然就解决了。用户登录后,随后的每个登录请求中都会带上这个凭证信息,将其作为登录凭证。此方案的优点是凭证在客户端保存,不需要服务端的参与和维护。但是这也是此方案的缺点,由于服务端无法管理本地的凭证,使其失效,如果认证凭证泄漏或者被盗用将引起很严重的问题。(后面会详细解答)

1.3 token盗用问题。

    在jwt的生成过程中我们编码进了用户的真实ip,即使token在使用过程中被劫取,但当网关在鉴权中发现token中的ip和请求的ip不同,也会自己拒绝掉。

1.4 如何防止短信验证码频繁刷新或被劫取

关于频繁刷新,这个比较好解决,针对每次短信验证码,只需要在Redis等缓存中存储对应的key值,这样就可以防止频繁刷新。关于验证码过期,也只需要用另外用一个key存储即可。
如何防止验证码被劫取,这就需要我们生成短信验证码的时候,同时生成一个令牌(如uuid),存储在手机设备上,当验证时一起带上令牌。因为,其他手机没有本验证码对应的令牌,因此无法完成登录。

1.5单点故障问题

        在整个微服务系统中,网关是所有流量的入口,所有的请求都要经过网关鉴权后再路由到不同的微服务中。在最开始,我们的登录系统也是耦合在平台业务支撑系统中。慢慢的登录功能趋于稳定,但是平台业务支撑部分功能迭代周期长,每次更新必然影响到用户的登录。再加上业务系统频繁访问平台的流量也会同时降低登录操作的效率。为了提高服务的可用性,解决登录单点故障问题,于是将登录系统单独拆分成独立的部分,这样一方面随着用户量的增加方便随时扩容,另一方面与业务分离开开来,大大提高了系统的可用性。

某安某大型国企项目Oauth2实践(二)

1.6如何支持多端登录

将每个客户端进行定义,为其分配唯一的标识符,将此标识符放在所有的请求头信息上携带,以供服务端进行判断。针对不同的客户端,不同的登录处理,包括token过期时间,token互斥等。为了保证header在传递过程中不丢失,Nginx 和gateway都要保留该header。
其他问题在稍后的代码分析中详细解答。

2.登录功能的扩展和实现

本登录方案是基于oauth2协议认证授权方案。流量的入口在网关,网关接收到登录请求后直接路由到登录服务
以下所有扩展基于登录服务实现。(敏感信息均以删除或者重写)

2.1 资源服务器配置

这个很简单,主要是url的访问策略配置。大家可以自己定义异常处理器和异常翻译器(略)。
@Configuration
@EnableResourceServer
protected class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf()
                .disable()
                // 开启跨域
                .cors().and()
                .exceptionHandling()
                .authenticationEntryPoint(customerAuthenticationEntryPoint())
                .accessDeniedHandler(customerOAuth2AccessDeniedHandler())
                .and()
                .authorizeRequests()
                .antMatchers(
                        "/oauth/**",
                        ...
                       )
                .permitAll()
                .anyRequest()
                .authenticated();
    }

2.2 认证服务器配置


@Configuration
@EnableAuthorizationServer

protected class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints
                //token存储
                .tokenStore(tokenStore())
                //异常翻译器
                .exceptionTranslator(webResponseExceptionTranslator)
                //token转化器
                .accessTokenConverter(accessTokenConverter())
                //自省处理器
                .userDetailsService(customerUserDetailService)
                //认证管理器
                .authenticationManager(authenticationManager);
    }

接下来依次分析。

2.2.1 token存储

因为我们使用的是jwt,所以采用的是jwtTokenStore
@Bean
public TokenStore tokenStore() {
    return new JwtTokenStore(accessTokenConverter());
}

2.2.2 异常翻译器

    定制自己的异常转化器。

2.2.3 AccessToken转化器

主要是自定义编码和解码token
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    ...
    return accessTokenConverter;
}

2.2.4 自省处理器

实现userDetailService,对用户信息的访问。
public class CustomUserDetailsService implements UserDetailsService {
    
    public UserDetails loadUserByUsername(String s){
            xxx
    }
}   

2.2.5 认证管理器

AuthenticationManager,使用框架自身的。

2.2.6 客户端认证

public class JdbcClientDetailsServiceEnhancer extends JdbcClientDetailsService {

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
       ...
    }
}
将客户端信息存储在数据库中

2.2.7 token状态机维护

public class CustomerTokenGranterAdapter implements TokenGranter {
    private TokenGranter tokenGranter;
    private TokenStatusProperties tokenStatusProperties;
 
    public CustomerTokenGranterAdapter(TokenGranter tokenGranter,
                                       TokenStatusProperties tokenStatusProperties){
        this.tokenGranter = tokenGranter;
        this.tokenStatusProperties=tokenStatusProperties;
    }

使用装饰器模式,将oauth2自带的tokenStore进行包装。

@Configuration
class TokenEndPointConfiguration implements ApplicationContextAware, InitializingBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        AuthorizationServerEndpointsConfiguration configuration =
                applicationContext.getBean(AuthorizationServerEndpointsConfiguration.class);
        TokenGranter tokenGranter = new CustomerTokenGranterAdapter(
                configuration.getEndpointsConfigurer().getTokenGranter(),tokenStatusProperties);

        TokenEndpoint tokenEndpoint = applicationContext.getBean(TokenEndpoint.class);
        tokenEndpoint.setTokenGranter(tokenGranter);
    }
这样整个授权体系最终用到的就是我们自定义的tokenGranter,将token状态维护在redis中。

3.Oauth2登录流程源码分析


接下来通过源码分析来感受自定义配置在源码中的体现。(主要分析密码登录)
正式处理登录请求,用的security的默认端口:/oauth/token,这是spring-security-oauth2的核心所在,配置在TokenEndpoint中
public class TokenEndpoint extends AbstractEndpoint {

    @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
    Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        //从请求中获取clientId
        String clientId = getClientId(principal);
        //根据clientId获取clientSecret
        ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
        //组装认证请求。
        TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
        //获取tokenGrant开始授权操作
        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        if (token == null) {
            throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
        }
        return getResponse(token);

    }

3.1 客户端认证

❶根据clientId获取clientSecret,这里getClientDetailsService()方法就会我们注入的自定义的客户端认证器JdbcClientDetailsServiceEnhancer,然后根据clientId从数据库查询到clientSecret。
❷组装认证请求
TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);
❸获取tokenGrant开始授权操作

   getTokenGranter()就是我们前面自定义的CustomerTokenGranterAdapter,然后里面注入的是Spring security自带的tokenGranter.

某安某大型国企项目Oauth2实践(二)

然后用了一个CompositeTokenGranter来委托代理所有默认的TokenGranter,即getDefaultTokenGranters()
private List<TokenGranter> getDefaultTokenGranters() {
    ClientDetailsService clientDetails = clientDetailsService();
    AuthorizationServerTokenServices tokenServices = tokenServices();
    AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
    OAuth2RequestFactory requestFactory = requestFactory();
    List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
    //授权码模式
    tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
            requestFactory));
    //token刷新        
    tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
    //隐式模式
    ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
    tokenGranters.add(implicit);
    //客户端模式
    tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
    //密码模式
    if (authenticationManager != null) {
        tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
                clientDetails, requestFactory));
    }
    return tokenGranters;
}
这么多tokenGranter,如何才能找到想要的呢?

某安某大型国企项目Oauth2实践(二)

我们看代理类CompositeTokenGranter的grant方法

某安某大型国企项目Oauth2实践(二)

遍历每个tokenGranter,调用grant方法。grant方法会调用到抽象类AbstractTokenGranter的实现。

public abstract class AbstractTokenGranter implements TokenGranter {
    
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

        // 判断grantType是否匹配
        if (!this.grantType.equals(grantType)) {
            return null;
        }
        String clientId = tokenRequest.getClientId();
        //验证客户端
        validateGrantType(grantType, client);
        ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        //获取token
        return getAccessToken(client, tokenRequest);

    }

❶ 每个grantType对应一个tokenGranter。

eg.

某安某大型国企项目Oauth2实践(二)

     ❷validateGrantType 验证客户端的支持的类型。

某安某大型国企项目Oauth2实践(二)

  这一块关于tokenGranter的设计就融入了组合模式,代理模式,和抽象化的思想。我们在碰到类似处理场景可以借鉴,可以优化if else代码。

3.2 认证获取accessToken

我们看getAccessToken()方法。
public abstract class AbstractTokenGranter implements TokenGranter {
    
    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }

3.2.1 组装认证对象

基于不同的许可类型,认证对象也会不同,同样getOAuth2Authentication也会会子类重写。

基于密码模式ResourceOwnerPasswordTokenGranter
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
    Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
    String username = parameters.get("username");
    String password = parameters.get("password");

    // Protect from downstream leaks of password
    parameters.remove("password");

    Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
    ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
    try {
        userAuth = authenticationManager.authenticate(userAuth);
    }
    
    OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);        
    return new OAuth2Authentication(storedOAuth2Request, userAuth);
}

从请求参数中或者用户名和密码后,开始认证,调用的则是核心类:认证管理器AuthenticationManager。(可以参考上一篇关于核心类的介绍)

默认的实现是ProviderManager
public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {

    for (AuthenticationProvider provider : getProviders()) {
        if (!provider.supports(toTest)) {
            continue;
        }
        try {
            result = provider.authenticate(authentication);
            ...
        }    
    }
}
和tokenGranter一样,每个不同的token类型也会对应不同的AuthenticationProvider,如我们上面创建的是UsernamePasswordAuthenticationToken,则对应的是抽象类AbstractUserDetailsAuthenticationProvider
public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authentication));

    }

而它的默认实现是DaoAuthenticationProvider,其他token类型的provider还有

某安某大型国企项目Oauth2实践(二)

如RememberMe的token类型是RememberMeAuthenticationToken

这里我们注入的是自定义provider: CustomerAuthenticationProvider.

我们继续看AuthenticationProvider的authenticate方法。这里调用的还是抽象类AbstractUserDetailsAuthenticationProvider的方法。

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {

    // Determine username
    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();

        try {
           // 自省处理,根据用户名获取用户认证信
            user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
        }
    }

    try {
        //前置检查,比如用户是否被锁等
        preAuthenticationChecks.check(user);
        //额外检查
        additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
    }
    postAuthenticationChecks.check(user);
    //创建认证对象
    return createSuccessAuthentication(principalToReturn, authentication, user);
    }

这里面的方法均可以被重写覆盖的。用户可以自定义校验方式。

❶retrieveUser 获取用户信息,这是springSecurity最核心的方法之一了。用户可以实现自己的自省方式,去定义用户信息的来源。如db,redis等。

❷preAuthenticationChecks.check 前置检查,比如检查用户是否被锁,是否启用等。

❸additionalAuthenticationChecks 额外检查,比如不同的登录方式其他信息的校验,短信验证码,签名证书,指纹信息等。

❹创建Authentication 认证对象

前面一篇也介绍过和userDetail(自省返回的数据)的区别

某安某大型国企项目Oauth2实践(二)

现在万事具备,只等着生成token了。

3.2.2 创建accessToken

public abstract class AbstractTokenGranter implements TokenGranter {
    
    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }

这里的tokenservice 默认实现是DefaultTokenServices

public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
        ConsumerTokenServices, InitializingBean {
        
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {

        OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken = null;
        if (existingAccessToken != null) {
            if (existingAccessToken.isExpired()) {
                if (existingAccessToken.getRefreshToken() != null) {
                    refreshToken = existingAccessToken.getRefreshToken();                
                    tokenStore.removeRefreshToken(refreshToken);
                }
                tokenStore.removeAccessToken(existingAccessToken);
            }
            else {
                // Re-store the access token in case the authentication has changed
                tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }
        }

        if (refreshToken == null) {
            refreshToken = createRefreshToken(authentication);
        }

        else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
            if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = createRefreshToken(authentication);
            }
        }
        //创建accessToken
        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
        //存储accessToken
        tokenStore.storeAccessToken(accessToken, authentication);
        // In case it was modified
        refreshToken = accessToken.getRefreshToken();
        if (refreshToken != null) {
            tokenStore.storeRefreshToken(refreshToken, authentication);
        }
        return accessToken;

    }                     

这里我们没有使用refreshToken机制,token的过期是通过redis控制的,后面再介绍。

❶createAccessToken


private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
    DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
    int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
    if (validitySeconds > 0) {
        token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
    }
    token.setRefreshToken(refreshToken);
    token.setScope(authentication.getOAuth2Request().getScope());

    return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
    }
默认的token是随机生成的uuid,跟用户信息没有关联。然后有效期在token中维护。
在前面的配置扩展中讲到,我们使用的是jwtToken,我们注入了jwt的accessTokenEnhancer:JwtAccessTokenConverter

public class JwtAccessTokenConverter implements TokenEnhancer, AccessTokenConverter, InitializingBean {

    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
            DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
            Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
            String tokenId = result.getValue();
            if (!info.containsKey(TOKEN_ID)) {
                info.put(TOKEN_ID, tokenId);
            }
            else {
                tokenId = (String) info.get(TOKEN_ID);
            }
            result.setAdditionalInformation(info);
            result.setValue(encode(result, authentication));
            OAuth2RefreshToken refreshToken = result.getRefreshToken();
            if (refreshToken != null) {
                DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
                encodedRefreshToken.setValue(refreshToken.getValue());
                // Refresh tokens do not expire unless explicitly of the right type
                encodedRefreshToken.setExpiration(null);
                try {
                    Map<String, Object> claims = objectMapper
                            .parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
                    if (claims.containsKey(TOKEN_ID)) {
                        encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
                    }
                }
                catch (IllegalArgumentException e) {
                }
                Map<String, Object> refreshTokenInfo = new LinkedHashMap<String, Object>(
                        accessToken.getAdditionalInformation());
                refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
                refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
                encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
                DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
                        encode(encodedRefreshToken, authentication));
                if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
                    Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
                    encodedRefreshToken.setExpiration(expiration);
                    token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
                }
                result.setRefreshToken(token);
            }
            return result;
        }
主要看encode(result, authentication)方法,填充jwt token信息
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        String content;
        try {
            content = objectMapper.formatMap(tokenConverter.convertAccessToken(accessToken, authentication));
        }
        catch (Exception e) {
            throw new IllegalStateException("Cannot convert access token to JSON", e);
        }
        String token = JwtHelper.encode(content, signer).getEncoded();
        return token;
    }
这里会获取token转换器,注入的是我们自定义的CustomerUserAuthenticationConverter,来自定义需要编码进token的用户信息。
public class CustomerUserAuthenticationConverter extends DefaultUserAuthenticationConverter{

 @Override
    public Map<String, ?> convertUserAuthentication(Authentication authentication) {
        Map<String, Object> response = new LinkedHashMap<String, Object>();
        //编码用户信息
        return response;
    }
    
    
    @Override
    public Authentication extractAuthentication(Map<String, ?> map) {
       ...
       //抽取用户信息
    }

convertUserAuthentication 方法用来编码用户信息到jwt中,比如用户的身份标识Id等。

相应的还有一个方法extractAuthentication ,用来抽取用户编码进token内的信息。主要用在认证过程中。
   自定义的用户信息编码到jwt payload后,然后通过JwtHelper.encode(content, signer).getEncoded(),生成了携带用户信息的jwt token。

3.3 token状态维护

前面的一步主要是通过tokenGranter生成了accessToken

public abstract class AbstractTokenGranter implements TokenGranter {
    
    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }

在讲第二步关于token过期时间和refreshToken时,我们没有用到默认的token状态维护,而是采用了redis去维护token.更加简单透明的维护token的状态。

然后整个生成token到tokenGranter这一步也就走完了,如何去切入呢?

public class TokenEndpoint extends AbstractEndpoint {

    @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
    Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        //从请求中获取clientId
        String clientId = getClientId(principal);
        //根据clientId获取clientSecret
        ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
        //组装认证请求。
        TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
        //获取tokenGrant开始授权操作
        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        if (token == null) {
            throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
        }
        return getResponse(token);

    }

也就是说我们切入tokenGranter,而且还要保证默认的granter正常执行,相当于做了后置增强操作。如果你对Spring源码比较了解,那你可能就会联想到InitializingBean,在bean的生命周期中会起到增强的效果。

于是,扩展就很容易实现了。

@Configuration
class TokenEndPointConfiguration implements ApplicationContextAware, InitializingBean {

    private ApplicationContext applicationContext;
    @Autowired
    private LoginProperties loginProperties;
    @Autowired
    private TokenStatusProperties tokenStatusProperties;

    @Override
    public void afterPropertiesSet() throws Exception {
        //获取认证服务端点配置类
        AuthorizationServerEndpointsConfiguration configuration =
                applicationContext.getBean(AuthorizationServerEndpointsConfiguration.class);
        //用自定义的tokerGranter包装默认实现        
        TokenGranter tokenGranter = new CustomerTokenGranterAdapter(
                configuration.getEndpointsConfigurer().getTokenGranter(),tokenStatusProperties);

        TokenEndpoint tokenEndpoint = applicationContext.getBean(TokenEndpoint.class);
        //更换tokenGranter用自定义的tokenGranter
        tokenEndpoint.setTokenGranter(tokenGranter);
    }

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

❶获取认证服务端点配置类,SpringSecurity的核心配置都可以通过配置类来生成。

❷用自定义的tokerGranter包装默认实现 

 这里采用了装饰模式。

❸用自定义的tokenGranter更换tokenGranter

这样就用自定义的tokenGranter来增强原始的类了。

4.Token相关问题

4.1 多端多设备登录问题

多端应用一般是允许同时登录的,比如QQ的同一个账号可以同时登录PC端,手机端。而相同端的不同设备是不允许登录的。如同一个微信号在手机A上登录了,在手机B上再登录,就会自动把A设备剔除。

4.1.1 key的设计


    为了区分不同端的设备,我们需要给用户的请求header上加上设备标识。在传递过程中不能丢失。同时在生成token后,存入redis时,要带上标识。
这个时候关于token redis key的设计便是:(ps:和真实有做区分,不要想着去攻击我们系统了。。。。)

某安某大型国企项目Oauth2实践(二)

登陆后,首先会往redis中存入该token。同时用set 集合将相同用户的token放入redis  set 集合中。

对应的token  set key为 :

某安某大型国企项目Oauth2实践(二)

4.1.2 互斥操作

当开启多端互斥开关,则在登录时,会首先将相同端账号的token标记为revoke,(即被挤下线的标识)同时清除set key里相同端账号的其他token。并单独将revoke的token维护在另外一个单独的key中,方便网关鉴权时好区分和token超时的情况,以给用户友好的提示。

revoke key :

某安某大型国企项目Oauth2实践(二)

流程图如下:


某安某大型国企项目Oauth2实践(二)

某安某大型国企项目Oauth2实践(二)

4.2 token盗用问题


为了防止jwt token被盗用,我们在token中编码进了ip,这样,即使token被盗用,因为不同设备ip不同,也会被网关拒绝。某安某大型国企项目Oauth2实践(二)

其他关于token鉴权问题,将在下篇网关鉴权再分析。


某安某大型国企项目Oauth2实践(二)
关注我的你,是最香哒!


原文始发于微信公众号(小李的源码图):某安某大型国企项目Oauth2实践(二)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/145316.html

(0)

相关推荐

  • 面试官:首屏加载速度慢怎么解决?

    前言 减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化 和 页面渲染优化。 下面主要简答地介绍一下首屏时间和优化首屏加载的部分做法,面试的时候不用答得很全面,…

    2022年11月23日
    00
  • 四、《图解HTTP》- 状态码

    状态码这一章内容过于贫乏,参考资料找了一个澳大利亚的博客,里面收录了HTTP的状态码介绍,为什么选这个作参考?一个是网站挺漂亮,另一个是做了一张长图容纳了常见的响应码,存到手机可以…

    2023年2月15日
    00
  • RabbitMQ第二弹-延时队列

    什么是延时队列 延迟队列首先它是一个队列,作为队列它的第一个特征是「有序的」,而之所以它被称为延时队列它还有一个更重要的特性就是「延时」。对于普通队列而言,如果有消费者订阅队列消费…

    2023年1月11日
    00
  • 5个 ChatGPT 功能,帮助你提升日常编码效率

    ChatGPT 作为最快完成亿活用户的应用,最近真的是火出天际了。今天分享5个 ChatGPT 功能,来提升我们的日常工作以及如何使用它提高代码质量。 ChatGPT 的出现,彻底…

    2023年2月14日
    00
  • OMS系统实战的三两事

    🔰 全文字数 : 9K+  🕒 阅读时长 : 15min 📋 关键词汇 :  OMS / 供应链 / 库存管理 / 单据业务 / 心得总结 1. 前言 这是一篇…

    2023年3月9日
    00
  • nginx sticky 实现基于cookie 的负载均衡

    本篇主要介绍一下 Nginx 的第三方模块 sticky , 依靠它实现基于 cookie级别的负载均衡, 不依赖后端 前言 sticky 是一个nginx的第三方模块 它不在ng…

    2023年2月15日
    00
  • 分布式数据库中间件Mycat介绍

    从Cobar到Mycat,从闭源到开源,作为一个开源的分布式数据库中间件,Mycat已经被众多开源项目使用。本文简要介绍下Mycat的特性、基本架构以及分库分表和读写分离的配置。 …

    2022年12月26日
    00
  • Goodbye 2020,Welcome 2021 | 沉淀 2021

    引言 2021年,已开启二月的篇章,农历新年也张灯结彩而来,只不过要留守过年。在这辞旧迎新之际,踏入而立之年之时,正是算账的好时候,数一数今年的成长,讲一讲来年的期望,最重要的还是…

    2023年2月9日
    00
  • 数据结构小记【Python/C++版】——B树篇

    一,基础概念 B树也是一种自平衡搜索树,常用于数据库中索引的实现。 B树和AVL树的区别在于: B树是一种多路平衡查找树,B树的节点可以有两个以上的子节点(AVL树是二叉树,最多只…

    2023年3月27日
    00
  • 协程原理再议

    @ 反编译后代码分析 关于协程的一些理解 ❝ 协程挂起让异步代码可以像同步代码一样调用,但其本质还是同步,即协程体中的代码其实是同步。 ❞ ❝ 因为协程也只是对线程池的封装,所以需…

    2023年1月15日
    00

发表回复

登录后才能评论