前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Springboot 集成OAuth2.0密码模式简单配置

Springboot 集成OAuth2.0密码模式简单配置

作者头像
黑洞代码
发布2021-01-14 15:44:08
3.1K0
发布2021-01-14 15:44:08
举报

名词定义

在理解OAuth 2.0作用之前,需要了解几个专用名词:

(1)Third-party application:第三方应用程序,简称"客户端"(client);

(2)HTTP service:HTTP服务提供商,简称服务端;

(3)Resource Owner:资源所有者,简称"用户"(user);

(4)User Agent:用户代理,是指浏览器;

(5)Authorization server:认证服务器,即服务端专门用来处理认证的服务器;

(6)Resource server:资源服务器,即服务端存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。

知道了上面这些名词,就不难理解,OAuth的作用就是让客户端安全可控地获取用户的授权,与服务端进行互动。

OAuth2基本思路

OAuth在客户端与服务端之间,设置了一个授权层(authorization layer)。客户端不能直接登录服务端,只能通过登录授权层获取服务端资源,以此将用户与客户端区分开来。客户端登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。

客户端登录授权层以后,服务端根据令牌的权限范围和有效期,向客户端开放用户可访问的资源。

OAuth 2.0的运行流程如下图所示:

(1)用户打开客户端请求用户给予授权;

(2)用户返回客户端授权;

(3)客户端使用获得的授权,向认证服务器请求token;

(4)认证服务器对客户端进行认证以后,同意给予认证token;

(5)客户端使用token,向资源服务器请求获取资源;

(6)资源服务器确认token,并同意向客户端开放资源。

springboot集成OAuth2.0配置使用

A.pom.xml文件中添加OAuth2支持(springboot2.0已将oauth2.0与security整合在一起,只需添加一下配置即可):

B.授权服务器配置:自定义OAuth2客户端认证与授权;

代码语言:javascript
复制
/**
 * 授权服务器配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    /**
     * 认证管理器
     * @see SecurityConfig 的authenticationManagerBean()

     */
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    /**
     * 使用jwt或者redis<br>
     * 默认redis
     */
    @Value("${access_token.store-jwt:false}")
    private boolean storeWithJwt;

    /**
     * 登陆后返回的json数据是否追加当前用户信息
     * 默认false
     */
    @Value("${access_token.add-userinfo:false}")
    private boolean addUserInfo;

    @Autowired
    private RedisAuthorizationCodeServices redisAuthorizationCodeServices;

    @Autowired
    private RedisClientDetailsService redisClientDetailsService;

    /**
     * 令牌存储
     */
    @Bean
    public TokenStore tokenStore() {   
        if (storeWithJwt) {
            return new JwtTokenStore(accessTokenConverter());
        }

        RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
        // 自定义获取Authkey(implements AuthenticationKeyGenerator)
        // 解决同一username每次登陆access_token都相同的问题       redisTokenStore.setAuthenticationKeyGenerator(new RandomAuthenticationKeyGenerator());
        return redisTokenStore;
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {    endpoints.authenticationManager(this.authenticationManager);
        endpoints.tokenStore(tokenStore());
  // 授权码模式下,code存储
  //  endpoints.authorizationCodeServices(new JdbcAuthorizationCodeServices(dataSource));        endpoints.authorizationCodeServices(redisAuthorizationCodeServices);
        if (storeWithJwt) {      
             endpoints.accessTokenConverter(accessTokenConverter());
        } else {
            // 将当前用户信息追加到登陆后返回数据里
            endpoints.tokenEnhancer((accessToken, authentication) -> {
                addLoginUserInfo(accessToken, authentication);
                return accessToken;
            });
        }
 }

    /**
     * 将当前用户信息追加到登陆后返回的json数据里<br>
     * 通过参数access_token.add-userinfo控制<br>
     *
     * @param accessToken
     * @param authentication
     */
    private void addLoginUserInfo(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {

        if (!addUserInfo) {
            return;
        }

        if (accessToken instanceof DefaultOAuth2AccessToken) {
            DefaultOAuth2AccessToken defaultOAuth2AccessToken = (DefaultOAuth2AccessToken) accessToken;
            Authentication userAuthentication = authentication.getUserAuthentication();
            Object principal = userAuthentication.getPrincipal();
            if (principal instanceof LoginAppUser) {
                LoginAppUser loginUser = (LoginAppUser) principal;
                Map<String, Object> map = new HashMap<>(defaultOAuth2AccessToken.getAdditionalInformation()); // 旧的附加参数
                map.put("loginUser", loginUser); // 追加当前登陆用户                defaultOAuth2AccessToken.setAdditionalInformation(map);

            }

        }

    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {

        security.allowFormAuthenticationForClients(); // 允许表单形式的认证

    }

    /**

     * 我们将client信息存储到oauth_client_details表里<br>
     * 并将数据缓存到redis
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    // 这里优化一下,详细看下redisClientDetailsService这个实现类
        clients.withClientDetails(redisClientDetailsService)       redisClientDetailsService.loadAllClientToCache();
    }

    @Autowired
    public UserDetailsService userDetailsService;

    /**

     * jwt签名key,可随意指定<br>

     * 如配置文件里不设置的话,冒号后面的是默认值

     */

    @Value("${access_token.jwt-signing-key:lihua}")
    private String signingKey;

    /**

     * Jwt资源令牌转换器<br>

     * 参数access_token.store-jwt为true时用到

     *

     * @return accessTokenConverter

     */

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {

       JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter() {

            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {

                OAuth2AccessToken oAuth2AccessToken = super.enhance(accessToken, authentication);

                addLoginUserInfo(oAuth2AccessToken, authentication); // 将当前用户信息追加到登陆后返回数据里

                return oAuth2AccessToken;

            }

        };

        DefaultAccessTokenConverter defaultAccessTokenConverter = (DefaultAccessTokenConverter) jwtAccessTokenConverter

                .getAccessTokenConverter();

        DefaultUserAuthenticationConverter userAuthenticationConverter = new DefaultUserAuthenticationConverter();

        userAuthenticationConverter.setUserDetailsService(userDetailsService);

        defaultAccessTokenConverter.setUserTokenConverter(userAuthenticationConverter);

        // 这里务必设置一个,否则多台认证中心的话,一旦使用jwt方式,access_token将解析错误

        jwtAccessTokenConverter.setSigningKey(signingKey);

        return jwtAccessTokenConverter;

    }

}

C.资源服务配置:设置受保护的资源和可访问的资源

代码语言:javascript
复制
@EnableResourceServer
@EnableWebSecurity
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

  @Override
  public void configure(HttpSecurity http) throws Exception {
    http.csrf().disable().exceptionHandling()
        .authenticationEntryPoint(
            (request, response, authException)
            /*-> response.sendRedirect("/hello/get"))*/
            -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "no auth"))
        .and().authorizeRequests()
        .antMatchers(PermitAllUrl.permitAllUrl("/hello/**","/login","/oauth/token")).permitAll()         // 放开权限的url ,"/hello/**"
        .anyRequest().authenticated().and().httpBasic();
  }

  /**
  * 自定义密码加密机制
  */
  @Bean
  public BCryptPasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

}  

D.自定义RedisClientDetailsService类

代码语言:javascript
复制
/**

 * 将oauth_client_details表数据缓存到redis,毕竟该表改动非常小,而且数据很少,这里做个缓存优化

 * 如果有通过界面修改client的需求的话,不要JdbcClientDetailsService了,请用该类,否则redis里有缓存

 * 如果手动修改了该表的数据,请注意清除redis缓存,是hash结构,key是client_details

 */
 @Slf4j
@Service
public class RedisClientDetailsService extends JdbcClientDetailsService {
    @Autowired
    private CacheManager cacheManager;
    public RedisClientDetailsService(DataSource dataSource) {
        super(dataSource);
    }
    /**
     * 缓存client的redis key,这里是hash结构存储
     */
    private static final String CACHE_CLIENT_KEY = "client_details";
    @Override
    public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
        ClientDetails clientDetails = null;
        // 先从redis获取
        Cache cache = cacheManager.getCache(CACHE_CLIENT_KEY);
        ValueWrapper valueWrapper = cache.get(clientId);       
        if (valueWrapper != null) {
          clientDetails = (ClientDetails) valueWrapper.get();
        } else {
             clientDetails = super.loadClientByClientId(clientId);
              if (clientDetails != null) {// 写入redis缓存
                cache.put(clientId, clientDetails);
                  log.info("缓存clientId:{},{}", clientId, clientDetails);
              }
        }

        return clientDetails;
    }

    /**
     * 缓存client并返回client
     * @param clientId
     */
    private ClientDetails cacheAndGetClient(String clientId) {
        // 从数据库读取
        ClientDetails clientDetails = super.loadClientByClientId(clientId);
        if (clientDetails != null) {// 写入redis缓存
          Cache cache = cacheManager.getCache(CACHE_CLIENT_KEY);
          cache.put(clientId, clientDetails);
            log.info("缓存clientId:{},{}", clientId, clientDetails);
        }

        return clientDetails;
    }

    @Override
    public void updateClientDetails(ClientDetails clientDetails) throws NoSuchClientException {
        super.updateClientDetails(clientDetails);
        cacheAndGetClient(clientDetails.getClientId());
    }

    @Override
    public void updateClientSecret(String clientId, String secret) throws NoSuchClientException {
        super.updateClientSecret(clientId, secret);
        cacheAndGetClient(clientId);
    }

    @Override
    public void removeClientDetails(String clientId) throws NoSuchClientException {
        super.removeClientDetails(clientId);
        removeRedisCache(clientId);
    }

    /**
     * 删除redis缓存
     *
     * @param clientId
     */
    private void removeRedisCache(String clientId) {
      Cache cache = cacheManager.getCache(CACHE_CLIENT_KEY);
      cache.evict(clientId);
        
    }

    /**
     * 将oauth_client_details全表刷入redis
     */
    public void loadAllClientToCache() {
      Cache cache = cacheManager.getCache(CACHE_CLIENT_KEY);
        
        //log.info("将oauth_client_details全表刷入redis");
        List<ClientDetails> list = super.listClientDetails();
        if (CollectionUtils.isEmpty(list)) {
            log.error("oauth_client_details表数据为空,请检查");
            return;
        }
        list.parallelStream().forEach(client -> {
          cache.put(client.getClientId(), client);
        });
    }
   

}

E:设置登录拦截

代码语言:javascript
复制
public class LoginFilter implements Filter {

   @Override
    public void destroy() {
        System.out.println("--------------过滤器销毁------------");
    }
 
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
            
      String code =request.getParameter(CommonController.VALIDATE_CODE);
      
      HttpSession session = ((HttpServletRequest)request).getSession(true);
      String ocode = (String) session.getAttribute(CommonController.VALIDATE_CODE);
      if(ocode==null || code ==null || !StringUtils.equalsIgnoreCase(ocode, code)) {
        
        response.setCharacterEncoding("utf-8");
      response.setContentType("application/json;charset=utf-8");
      response.getWriter().write(JsonUtils.objectToJsonWhitI18N( Result.BAD_REQUEST(ContentConstant.VCODE_ERROR)));
      return;
      }
      //request.gets
      //request.getSession()
      HashMap<String, Object> params = new HashMap<>();
    
      params.put("grant_type", new String[] {"password"});
      params.put("client_id", new String[] {"system"});
      params.put("client_secret", new String[] {"system"});
      params.put("scope", new String[] {"app"});
      ParameterRequestWrapper wrapRequest=new ParameterRequestWrapper((HttpServletRequest)request,params);
        chain.doFilter(wrapRequest, response);
        
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {

        System.out.println("--------------过滤器初始化------------");

    }

}

F:使用post访问链接:http://ip:port/oauth/token

参数:

grant_type=password(密码模式)

client_id=system(自定义)

client_secret=system(自定义)

scope=app(自定义)

username=数据库中设置的自定义用户名

password=数据库中设置的自定义密码

访问成功,则可获取如下结果:

注:

access_token:表示访问令牌,必选项;

token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型;

refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项;

expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间;

scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-05-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 落叶飞翔的蜗牛 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
访问管理
访问管理(Cloud Access Management,CAM)可以帮助您安全、便捷地管理对腾讯云服务和资源的访问。您可以使用CAM创建子用户、用户组和角色,并通过策略控制其访问范围。CAM支持用户和角色SSO能力,您可以根据具体管理场景针对性设置企业内用户和腾讯云的互通能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档