Need help with springcloud-oauth2?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

copoile
179 Stars 73 Forks MIT License 95 Commits 7 Opened issues

Description

本项目基于spring-cloud-starter-oauth2搭建的认证中心和资源服务器的微服务项目,项目不仅仅简单的demo,项目的出发点在于实战应用。本项目为笔者花了不少时间和精力整理出来的,只需要稍微调整就可应用于实际项目当中,并且项目包含大量注释,不仅可以让你会用,也可让你了解到一些流程、一些原理上的东西。认证中心完成密码模式、授权码模式、刷新token模式、简化模式、以及自定义的手机号验证码模式。

Services available

!
?

Need anything else?

Contributors list

No Data

简介

本项目基于spring-cloud-starter-oauth2搭建的认证中心和资源服务器的微服务项目,项目不仅仅简单的demo,项目的出发点在于实战应用。本项目为笔者花了不少时间和精力整理出来的,只需要稍微调整就可应用于实际项目当中,并且项目包含大量注释,不仅可以让你会用,也可让你了解到一些流程、一些原理上的东西。认证中心完成密码模式、授权码模式、刷新token模式、简化模式、以及自定义的手机号验证码模式。

国内Gitee:https://gitee.com/copoile/springcloud-oauth2.git

如果大家有什么疑问或不懂的地方可以issue 里提问。 有什么说得不对或不合理的地方也欢迎指出。希望对你有所帮助呦~ ^_^

分支说明

目前分支有master和prod,master分支为快速上手,未配置数据库,内容偏教程流程理解,prod分支新建数据库相关表,并实现认证相关数据查询,同时实现统一异常处理,统一Api。 prod分支-用户名:admin/123456,客户端:web/123456

功能

- 密码模式
- 自定义手机验证码模式
- 授权码模式
- 简化模式
- 刷token模式
- 退出测试接口
- 简单授权页面
- 不需要accessToken测试接口
- 需要accessToken测试接口
- 需要特定权限测试接口
- scope测试接口

开发环境

  • JDK 1.8 +
  • Maven 3.5 +
  • IntelliJ IDEA ULTIMATE 2018.2 + (注意:建议使用 IDEA 开发,同时保证安装
    lombok
    插件,如果是eclipse也要确保安装了
    lombok
    插件
    )
  • Redis 3.0 +

运行方式

  1. git clone https://github.com/copoile/springcloud-oauth2.git
  2. 使用 IDEA 打开 clone 下来的项目
  3. 项目启动顺序: eureka-server > auth-server > resource-server > 注意:auth-server依赖redis服务,记得先启动redis服务哦~

认证验证流程

这里简单做下密码模式的认证和accessToken验证流程,手机号模式跟这个类型,授权码模式和简化模式稍微有点不一样,授权码模式和简化模式都是先跳到认证中心的授权页面,授权成功后回调回调地址,并且携带参数code或accessToken。

认证中心核心代码

AuthorizationConfig.java

/**
 * 认证配置
 * @author: yaohw
 * @create: 2019-09-30 16:12
 **/
@Configuration
@EnableAuthorizationServer
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private UserDetailsServiceImpl userDetailsService;

@Autowired
private ClientDetailsServiceImpl clientDetailsService;

@Autowired
private RedisConnectionFactory redisConnectionFactory;


@Autowired
private RedisTemplate<string object> redisTemplate;




/**
 * 配置token存储,这个配置token存到redis中,还有一种常用的是JwkTokenStore
 * jwt的缺点已发布令牌不可控
 * @return
 */
@Bean
public TokenStore tokenStore() {
    return new RedisTokenStore(redisConnectionFactory);
}

/**
 * 配置授权码模式授权码服务(存授权码和删除授权码),不配置默认为内存模式
 * @return
 */
@Primary
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
    return new RedisAuthorizationCodeServices(redisConnectionFactory);
}

/**
 * 配置客户端详情(根据客户的id查询客户端)
 * @param clients
 * @throws Exception
 */
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.withClientDetails(clientDetailsService);
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    // 采用token转jwt,并添加一些自定义信息(token增强)
    // 默认使用随机UUID生成的token
    TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
    tokenEnhancerChain.setTokenEnhancers(
            Arrays.asList(jwtAccessTokenConverter(),tokenEnhancer()));
    endpoints.tokenEnhancer(tokenEnhancerChain)
            // 配置token存储,一般配置redis存储
            .tokenStore(tokenStore())
            // 配置认证管理器
            .authenticationManager(authenticationManager)
            // 配置用户详情server,密码模式必须
            .userDetailsService(userDetailsService)
            // 配置授权码模式授权码服务,不配置默认为内存模式
            .authorizationCodeServices(authorizationCodeServices())
            // 配置grant_type模式,如果不配置则默认使用密码模式、简化模式、验证码模式以及刷新token模式,如果配置了只使用配置中,默认配置失效
            // 具体可以查询AuthorizationServerEndpointsConfigurer中的getDefaultTokenGranters方法
            .tokenGranter(tokenGranter(endpoints));
    //  配置TokenServices参数
    DefaultTokenServices tokenServices = new DefaultTokenServices();
    tokenServices.setTokenStore(endpoints.getTokenStore());
    // 是否支持刷新Token
    tokenServices.setSupportRefreshToken(true);
    tokenServices.setReuseRefreshToken(true);
    tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
    tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
    // 设置accessToken和refreshToken的默认超时时间(如果clientDetails的为null就取默认的,如果clientDetails的不为null取clientDetails中的)
    tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(2));
    tokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30));
    endpoints.tokenServices(tokenServices);

}



/**
 * jwt格式封装token
 * @return
 */
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
    JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
    // 设置jwt加解密秘钥,不设置会随机一个
    jwtAccessTokenConverter.setSigningKey("yaohw");
    return jwtAccessTokenConverter;
}

/**
 * token增强,添加一些元信息
 *
 * @return TokenEnhancer
 */
@Bean
public TokenEnhancer tokenEnhancer() {
    return (accessToken, authentication) -&gt; {
        final Map<string object> additionalInfo = new HashMap&lt;&gt;(2);
        additionalInfo.put("license", "yaohw");
        UserDetailImpl user = (UserDetailImpl) authentication.getUserAuthentication().getPrincipal();
        if (user != null) {
            additionalInfo.put("username", user.getUsername());
        }
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        return accessToken;
    };
}


@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    security
            .allowFormAuthenticationForClients()
            .tokenKeyAccess("isAuthenticated()")
            .checkTokenAccess("permitAll()");
}

/**
 * 创建grant_type列表
 * @param endpoints
 * @return
 */
private TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
    List<tokengranter> list = new ArrayList&lt;&gt;();
    // 这里配置密码模式、刷新token模式、自定义手机号验证码模式、授权码模式、简化模式
    list.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
    list.add(new RefreshTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
    list.add(new MobileCodeTokenGranter(authenticationManager,endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
    list.add(new AuthorizationCodeTokenGranter(endpoints.getTokenServices(),endpoints.getAuthorizationCodeServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));
    list.add(new ImplicitTokenGranter(endpoints.getTokenServices(),endpoints.getClientDetailsService(),endpoints.getOAuth2RequestFactory()));
    return new CompositeTokenGranter(list);
}

}

SecurityConfigurerAdapter.java

/**
 * security web安全配置,spring-cloud-starter-oauth2依赖于security
 *  默认情况下SecurityConfigurerAdapter执行比ResourceServerConfig先
 * @author: yaohw
 * @create: 2019-09-25 16:49
 */
@Configuration
@EnableWebSecurity
public class SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsServiceImpl userDetailsService;

@Autowired
private StringRedisTemplate stringRedisTemplate;


/**
 * 配置认证管理器
 *
 * @return
 * @throws Exception
 */
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}


/**
 * 配置密码加密对象(解密时会用到PasswordEncoder的matches判断是否正确)
 * 用户的password和客户端clientSecret用到,所以存的时候存该bean encode过的密码
 *
 * @return
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

/**
 * 这里是对认证管理器的添加配置
 *
 * @param auth
 * @throws Exception
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(provider())
            .userDetailsService(userDetailsService)
            .passwordEncoder(new BCryptPasswordEncoder());
}

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/css/**","/static/**");
}

/**
 *  安全请求配置,这里配置的是security的部分,这里配置全部通过,安全拦截在资源服务的配置文件中配置,
 *  要不然访问未验证的接口将重定向到登录页面,前后端分离的情况下这样并不友好,无权访问接口返回相关错误信息即可
 * @param http
 * @return void
 */
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .formLogin().loginPage("/login")
            .permitAll()
            .and().authorizeRequests().anyRequest().permitAll()
            .and().csrf().disable().cors();
}


/**
 * 自定义手机验证码认证提供者
 *
 * @return
 */
@Bean
public MobileCodeAuthenticationProvider provider() {
    MobileCodeAuthenticationProvider provider = new MobileCodeAuthenticationProvider();
    provider.setStringRedisTemplate(stringRedisTemplate);
    provider.setHideUserNotFoundExceptions(false);
    provider.setUserDetailsService(userDetailsService);
    return provider;
}

}

ResourceServerConfig.java

/**
 * 资源服务配置
 * @author: yaohw
 * @create: 2019-10-08 10:04
 **/
@Configuration
// 启用资源服务
@EnableResourceServer
// 启用方法级权限控制
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Log4j2
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

private static final String RESOURCE_ID = "auth-server";


/**
 *  配置资源接口安全,http.authorizeRequests()针对的所有url,但是由于登录页面url包含在其中,这么配置会进行token校验,校验不通过返回错误json,
 *  而授权码模式获取code时需要重定向登录页面,重定向过程并不能携带token,所有不能用http.authorizeRequests(),
 *  而是用requestMatchers().antMatchers(""),这里配置的是需要资源接口拦截的url数组
 * @param http
 * @return void
 */
@Override
public void configure(HttpSecurity http) throws Exception {
    http    //配置需要保护的资源接口
            .requestMatchers().antMatchers("/user","/test/need_token","/update","/logout","/test/need_admin","/test/scope")
            .and().authorizeRequests().anyRequest().authenticated();
}


@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    resources.resourceId(RESOURCE_ID).stateless(true);
}

}

资源服务配置文件

spring:
  application:
    name: resource-server

server: port: 8003

#服务器发现注册配置 eureka: client: serviceUrl: #配置服务中心(可配置多个,用逗号隔开) defaultZone: http://admin:[email protected]:9000/eureka/

##安全配置## security: oauth2: resource: id: resource-server ## user-info-uri和token-info-uri二选择即可 ## 如果配置了user-info-uri,该资源服务器使用userInfoTokenServices远程调用认证中心接口,通过认证中心的OAuth2AuthenticationProcessingFilter完成验证工作,一般设置user-info-uri即可 user-info-uri: http://127.0.0.1:8001/user prefer-token-info: false ## 该资源服务器使用RemoteTokenServices远程调用认证中心接口,注意一点就是如果使用token-info-uri那么就必须设置上clientId和clientSecret,通过CheckTokenEndpoint完成验证工作 #token-info-uri: http://127.0.0.1:8001/oauth/check_token #client: #client-secret: yaohw #client-id: yaohw

部分源代码讲解

认证(获取token)TokenEndpoint.java

    @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
    public ResponseEntity postAccessToken(Principal principal, @RequestParam
    Map parameters) throws HttpRequestMethodNotSupportedException {

    if (!(principal instanceof Authentication)) {
        throw new InsufficientAuthenticationException(
                "There is no client authentication. Try adding an appropriate authentication filter.");
    }

    // 根据当前请求获取到clientId
    String clientId = getClientId(principal);

    // 获取当前ClientDetailsService(就是我们在AuthorizationConfig中配置)然后根据clientId去数据库查询客户端详情
    ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

    // 将请求参数封装成TokenRequest
    TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
    // 请求的clientId与查出来的匹配
    if (clientId != null &amp;&amp; !clientId.equals("")) {
        // Only validate the client details if a client authenticated during this
        // request.
        if (!clientId.equals(tokenRequest.getClientId())) {
            // double check to make sure that the client ID in the token request is the same as that in the
            // authenticated client
            throw new InvalidClientException("Given client ID does not match authenticated client");
        }
    }
    // 校验客户端范围
    if (authenticatedClient != null) {
        oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
    }
    if (!StringUtils.hasText(tokenRequest.getGrantType())) {
        throw new InvalidRequestException("Missing grant type");
    }
    // 判断是否是简化模式(简化模式不是这个接口,走的是AuthorizationEndpoint类下的/oauth/authorize)
    if (tokenRequest.getGrantType().equals("implicit")) {
        throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
    }
    // 判断是否授权码模式,如果是,清空返回,因为授权码模式在第一步获取code的时候就将client信息缓存起来的,后面检验的是从缓存取出来补充完整
    if (isAuthCodeRequest(parameters)) {
        // The scope was requested or determined during the authorization step
        if (!tokenRequest.getScope().isEmpty()) {
            logger.debug("Clearing scope of incoming token request");
            tokenRequest.setScope(Collections.<string> emptySet());
        }
    }
    // 是否刷新token模式
    if (isRefreshTokenRequest(parameters)) {
        // A refresh token has its own default scopes, so we should ignore any added by the factory here.
        tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
    }
    // 这步是整个认证的关键,这里简单说下流程,首先她会根据当前请求的grantType找到对应的认证模式,
    // 比如密码模式的ResourceOwnerPasswordTokenGranter,
    // 然后对应的AbstractTokenGranter调用对应的grant方法,grant方法中又调用经过一系列调用,
    // 在getOAuth2Authentication方法中生成对应的AbstractAuthenticationToken,比如UsernamePasswordAuthenticationToken,
    // 然后认证管理器(就是我们在AuthorizationConfig中配置的AuthenticationManager)调用认证方法authenticationManager.authenticate(abstractAuthenticationToken)
    // AbstractAuthenticationToken和AuthenticationProvider是存在一一对应的关系
    // 比如UsernamePasswordAuthenticationToken和DaoAuthenticationProvider,
    // authenticationManager.authenticate()会根据传入的
    // AbstractAuthenticationToken找到对应的AuthenticationProvider,
    // 真正认证逻辑通过AuthenticationProvider来完成的,比如密码模式的DaoAuthenticationProvider,
    // 会去根据用户名查询出对应的用户,
    // 然后校验用户密码是否匹配,用户是否锁定过期等
    // 具体可查看DaoAuthenticationProvider和她继承的AbstractUserDetailsAuthenticationProvider
    // 理清上面的思路后,我们就可以自定义grantType
    // 就是定义一个继承AbstractTokenGranter的类重写getOAuth2Authentication方法
    // 该方法里面会用到AbstractAuthenticationToken和AuthenticationProvider
    // 我们再分别定义一个类分别继承对应的类即可(大概思路,具体查看代码)
    OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
    if (token == null) {
        throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
    }
    //这个没什么好说的,就是http请求响应体封装
    return getResponse(token);

}

验证token

了解过OAuth2的同学应该知道它有资源服务和认证中心服务,那么它怎么保护资源服务接口的呢?实际上不管认证中服务还是资源服务,当请求的接口需要安全校验时都会被OAuth2ClientAuthenticationProcessingFilter所拦截,只是拦截后做了不同的处理(取决于ResourceServerTokenServices的实例)。资源服务:拦截请求后会远程调用认证服务器的

http://127.0.0.1:8001/user
http://127.0.0.1:8001/oauth/check_token
,至于调用哪个取决于配置文件,如配置如下配置将远程调用
http://127.0.0.1:8001/user
(资源服务端我们也一般这么配置即可)
##安全配置##
security:
  oauth2:
    resource:
      id: resource-server
      user-info-uri: http://127.0.0.1:8001/user
      prefer-token-info: false

(拦截token校验)OAuth2AuthenticationProcessingFilter.java

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
            ServletException {
        final boolean debug = logger.isDebugEnabled();
        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;
        try {

        Authentication authentication = tokenExtractor.extract(request);
        Authentication authResult = authenticationManager.authenticate(authentication);
        ....略
    // 这步是校验token的关键,这里tokenServices是ResourceServerTokenServices实例,这里做怎么样的操作取决于注入的      
// ResourceServerTokenServices实例
    // 默认情况下ResourceServerTokenServices的实例是DefaultTokenServices
    // 认证中心使用的就是DefaultTokenServices,这个类做的就是tokenStore.readAccessToken(accessTokenValue)
    // 我们配置中心配置的tokenStore的是RedisTokenStore,所以实际上她做的就是从redis中读取出accessToken相关信息
    <!-- 分割线 --->
    // 上面说的DefaultTokenServices是认证中心token的处理,资源服务下:
    // 如果配置文件中配置的user-info-uri则ResourceServerTokenServices注入的实例将是UserInfoTokenServices的实例
    // 如果配置token-info-uri则ResourceServerTokenServices注入的实例将是RemoteTokenServices
    // 如果两者都配置了,优先UserInfoTokenServices
    // UserInfoTokenServices和RemoteTokenServices做的事都是远程调度认证中心相应的接口完成token的校验
    // 两者主要区别在于RemoteTokenServices需要配置clientId和clientSecret
    // RemoteTokenServices中有这么一句话:Null Client ID or Client Secret detected. Endpoint that requires authentication will reject     // request with 401 error.  // 具体请查看RemoteTokenServices和UserInfoTokenServices
// OAuth2AuthenticationManager.java
    String token = (String) authentication.getPrincipal();
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
    }
    }

postman接口测试截图

客户端Basic请求头

这里两种方式都是一样的,eWFvaHc6eWFvaHc=其实就是yaohw:yaohw,经过base64加密了一下

image

密码模式

image

自定义手机号验证码模式

注意:需要在redis中设置一个缓存,String类型,key为sms:code:你的手机号,值为短信验证码

image

image

授权码模式

授权码模式步骤一

授权码模式步骤一 会跳转到认证中心的授权页面,这里为方便展示参数才用postman,get请求,应在浏览器直接打开(带对应参数),授权成功后会回调回调地址,并且会携带code。 image

授权码模式步骤二(授权页面授权)

image

授权码模式步骤三(获取code)

授权码模式步骤四(根据code获取token)

简化模式

与授权码模式类似,不过回调后携带的参数不是code,还是access_token,比授权码模式少了一步.

步骤一

简化模式步骤一会跳转到认证中心的授权页面,这里为方便展示参数才用postman,get请求,应在浏览器直接打开(带对应参数),授权成功后会回调回调地址,并且会携带accessToken。

步骤二(授权页面授权)

image

步骤三

刷新token模式

image

License

MIT

Copyright (c) 2019-present Yaohw

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.