专栏首页算法之名OAuth2.0用户名,密码登录解析

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

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

下面是相关的类图

首先我们从请求认证开始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<OAuth2AccessToken> 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);
    }
}

数据库中数据如下

而我们需要在

@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分钟<br>
    * value为OAuth2Authentication序列化后的字节<br>
    * 因为OAuth2Authentication没有无参构造函数<br>
    * redisTemplate.opsForValue().set(key, value, timeout, unit);
    * 这种方式直接存储的话,redisTemplate.opsForValue().get(key)的时候有些问题,
    * 所以这里采用最底层的方式存储,get的时候也用最底层的方式获取
    */
   @Override
   protected void store(String code, OAuth2Authentication authentication) {
      redisTemplate.execute(new RedisCallback<Long>() {

         @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<OAuth2Authentication>() {

         @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;
   }
}

我们可以看到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<MediaType> 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" }

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • mysql 8不再需要手工升级检查表?

    The mysql_upgrade client is now deprecated. The actions executed by the upgrade ...

    Tuesday
  • 老程序员都干嘛去了?来看下国外的调查!

    在纽约,PyGotham每年召开之际,都会有超过600名程序员聚集在一起讨论工作。为了让会议更加多元化,组织者尽量邀请一些女性程序员以及各种肤色的程序员。

    Java技术栈
  • Spring 常犯的十大错误,打死都不要犯!

    译文:www.cnblogs.com/liululee/p/11235999.html

    Java技术栈
  • 会 Python 就能年薪 40w?答案早就写在 JD 上了...

    现在的职场竞争越来越激烈,不学上一两门新技能,保持自己知识更新,很容易被年轻后辈超越。有些人选择学一门外语,有些人选择学习职场上为人处事的能力。

    崔庆才
  • 记一次渗透挖洞提权实战

    摘要:这是一次挖掘cms通用漏洞时发现的网站,技术含量虽然不是很高,但是也拿出来和大家分享一下吧,希望能给一部分人带来收获。

    HACK学习
  • java使用poi读取excel文档的一种解决方案

    本人在学习使用java的过程中,需要验证一下excel表格里面的数据是否与数据库中的数据相等。由于数据太多,故想着用java读取excel数据再去数据库验证。上...

    八音弦
  • SQL存储过程

    木瓜煲鸡脚
  • 1.1 服务器安装操作系统

    Linux平台 Oracle 19c RAC安装指导: Part1:Linux平台 Oracle 19c RAC安装Part1:准备工作 Part2:Lin...

    Alfred Zhao
  • 记一次渗透实战

    趁nmap还在工作的时候,简单浏览了下网站的功能,伪静态,整个网站也没有什么动态功能

    HACK学习
  • Docker系列 | 分布式数据库etcd

    etcd 是 CoreOS 团队于 2013 年 6 月发起的开源项目,它的目标是构建一个高可用的分布式键值(key-value)数据库,基于 Go 语言实现。...

    Tinywan

扫码关注云+社区

领取腾讯云代金券