前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OAuth2.0用户名,密码登录解析

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

作者头像
算法之名
发布2019-08-20 17:28:03
3.9K0
发布2019-08-20 17:28:03
举报
文章被收录于专栏:算法之名算法之名

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" }

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档