简介: 在oauth2中授权服务器颁发过令牌后,用户就可以对资源进行访问,这时会有一个问题就是需要验证令牌,而前边文章中令牌使用的jwt格式,所以需要一个统一的资源管理器来验证令牌解析令牌
源码分析: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 public final class BearerTokenAuthenticationFilter extends OncePerRequestFilter { private final AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver; private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint(); private AuthenticationFailureHandler authenticationFailureHandler = (request, response, exception) -> { if (exception instanceof AuthenticationServiceException) { throw exception; } this.authenticationEntryPoint.commence(request, response, exception); }; private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource(); /** * Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s) * @param authenticationManagerResolver */ public BearerTokenAuthenticationFilter( AuthenticationManagerResolver<HttpServletRequest> authenticationManagerResolver) { Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null"); this.authenticationManagerResolver = authenticationManagerResolver; } /** * Construct a {@code BearerTokenAuthenticationFilter} using the provided parameter(s) * @param authenticationManager */ public BearerTokenAuthenticationFilter(AuthenticationManager authenticationManager) { Assert.notNull(authenticationManager, "authenticationManager cannot be null"); this.authenticationManagerResolver = (request) -> authenticationManager; } /** * Extract any * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer * Token</a> from the request and attempt an authentication. * @param request * @param response * @param filterChain * @throws ServletException * @throws IOException */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token; try { token = this.bearerTokenResolver.resolve(request); } catch (OAuth2AuthenticationException invalid) { this.logger.trace("Sending to authentication entry point since failed to resolve bearer token", invalid); this.authenticationEntryPoint.commence(request, response, invalid); return; } if (token == null) { this.logger.trace("Did not process request since did not find bearer token"); filterChain.doFilter(request, response); return; } BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token); authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); try { AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request); Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authenticationResult); SecurityContextHolder.setContext(context); if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authenticationResult)); } filterChain.doFilter(request, response); } catch (AuthenticationException failed) { SecurityContextHolder.clearContext(); this.logger.trace("Failed to process authentication request", failed); this.authenticationFailureHandler.onAuthenticationFailure(request, response, failed); } } /** * Set the {@link BearerTokenResolver} to use. Defaults to * {@link DefaultBearerTokenResolver}. * @param bearerTokenResolver the {@code BearerTokenResolver} to use */ public void setBearerTokenResolver(BearerTokenResolver bearerTokenResolver) { Assert.notNull(bearerTokenResolver, "bearerTokenResolver cannot be null"); this.bearerTokenResolver = bearerTokenResolver; } /** * Set the {@link AuthenticationEntryPoint} to use. Defaults to * {@link BearerTokenAuthenticationEntryPoint}. * @param authenticationEntryPoint the {@code AuthenticationEntryPoint} to use */ public void setAuthenticationEntryPoint(final AuthenticationEntryPoint authenticationEntryPoint) { Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null"); this.authenticationEntryPoint = authenticationEntryPoint; } /** * Set the {@link AuthenticationFailureHandler} to use. Default implementation invokes * {@link AuthenticationEntryPoint}. * @param authenticationFailureHandler the {@code AuthenticationFailureHandler} to use * @since 5.2 */ public void setAuthenticationFailureHandler(final AuthenticationFailureHandler authenticationFailureHandler) { Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null"); this.authenticationFailureHandler = authenticationFailureHandler; } /** * Set the {@link AuthenticationDetailsSource} to use. Defaults to * {@link WebAuthenticationDetailsSource}. * @param authenticationDetailsSource the {@code AuthenticationConverter} to use * @since 5.5 */ public void setAuthenticationDetailsSource( AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null"); this.authenticationDetailsSource = authenticationDetailsSource; } }
JwtAuthenticationProvider的 jwt的主流程处理中心
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public final class JwtAuthenticationProvider implements AuthenticationProvider { private final Log logger = LogFactory.getLog(getClass()); private final JwtDecoder jwtDecoder; private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter = new JwtAuthenticationConverter(); public JwtAuthenticationProvider(JwtDecoder jwtDecoder) { Assert.notNull(jwtDecoder, "jwtDecoder cannot be null"); this.jwtDecoder = jwtDecoder; } /** * Decode and validate the * <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer * Token</a>. * @param authentication the authentication request object. * @return A successful authentication * @throws AuthenticationException if authentication failed for some reason */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication; Jwt jwt = getJwt(bearer); AbstractAuthenticationToken token = this.jwtAuthenticationConverter.convert(jwt); token.setDetails(bearer.getDetails()); this.logger.debug("Authenticated token"); return token; } private Jwt getJwt(BearerTokenAuthenticationToken bearer) { try { return this.jwtDecoder.decode(bearer.getToken()); } catch (BadJwtException failed) { this.logger.debug("Failed to authenticate since the JWT was invalid"); throw new InvalidBearerTokenException(failed.getMessage(), failed); } catch (JwtException failed) { throw new AuthenticationServiceException(failed.getMessage(), failed); } } @Override public boolean supports(Class<?> authentication) { return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication); } public void setJwtAuthenticationConverter( Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter) { Assert.notNull(jwtAuthenticationConverter, "jwtAuthenticationConverter cannot be null"); this.jwtAuthenticationConverter = jwtAuthenticationConverter; } }
分析doFilterInternal方法我们可知:
请求会被 BearerTokenAuthenticationFilter 拦截器拦截,通过bearerTokenResolver解析出token字符串如果没有解析出来,则由下一个过滤器处理。如果失败将被AuthenticationEntryPoint统一拦截处理
解析出来则构建一个BearerTokenAuthenticationToken对象。
将HttpServletRequest传递给AuthenticationManagerResolver对象,由它选择出AuthenticationManager对象,然后将 BearerTokenAuthenticationToken传递给AuthenticationManager对象进行认证。AuthenticationManager对象的实现,取决于我们的token对象是JWT还是opaque token
在AuthenticationManager由多个Provider对Authentication进行处理最终返回结果,当对象为jwt时处理对象为JwtAuthenticationProvider,在JwtAuthenticationProvider会先通过jwtDecoder解析jwt字符串,最终对JwtAuthenticationConverter将jwt字符串处理的为AbstractAuthenticationToken
验证失败
清空 SecurityContextHolder 对象。
交由AuthenticationFailureHandler对象处理。
验证成功
将 Authentication对象设置到SecurityContextHolder中。
交由余下的过滤器继续处理。
具体实现: pom文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency>
ResourceServerConfiguration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @Configuration @EnableMethodSecurity @RequiredArgsConstructor public class ResourceServerConfiguration { @Autowired private RedisService redisService; private final HttpSecurity http; @Bean public SecurityFilterChain filterChain() throws Exception { http .authorizeHttpRequests(authorize -> authorize .antMatchers("/actuator/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .authenticationEntryPoint(luckAuthenticationEntryPoint()) .bearerTokenResolver(new LuckBearerTokenResolver(redisService)) .jwt()); return http.build(); } @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); grantedAuthoritiesConverter.setAuthorityPrefix(""); grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return jwtAuthenticationConverter; } @Bean public LuckAuthenticationEntryPoint luckAuthenticationEntryPoint() { return new LuckAuthenticationEntryPoint(); } @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Bean("pms") public PermissionHandler permissionHandler() { return new PermissionHandler(); } }
异常定义
1 2 3 4 5 6 7 8 9 @Getter public class LuckAuth2AuthencticationException extends OAuth2AuthenticationException { private final String code; public LuckAuth2AuthencticationException(ResponseStatusEnum status) { super(new OAuth2Error(status.getMsg()), status.getMsg()); this.code = status.getCode(); } }
检查异常处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class LuckAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override @SneakyThrows public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ServletServerHttpResponse servletServerHttpResponse = new ServletServerHttpResponse(response); AjaxResult<?> error; if(authException instanceof InvalidBearerTokenException){ servletServerHttpResponse.setStatusCode(HttpStatus.FAILED_DEPENDENCY); error = AjaxResult.error(ResponseStatusEnum.INVALID_GRANT, authException.getMessage()); }else{ servletServerHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED); error = AjaxResult.error(ResponseStatusEnum.UNAUTHORIZED, authException.getMessage()); } HttpEndpointUtils.writeWithMessageConverters(error, servletServerHttpResponse); } }
令牌适配器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 @RequiredArgsConstructor public class LuckBearerTokenResolver implements BearerTokenResolver { private final RedisService redisService; private static final Pattern AUTHORIZATION_PATTERN = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+=*)$", Pattern.CASE_INSENSITIVE); private boolean allowFormEncodedBodyParameter = false; private boolean allowUriQueryParameter = false; private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION; // public LuckBearerTokenResolver(RedisService redisService){ // this.redisService = redisService; // } @Override public String resolve(HttpServletRequest request) { final String authorizationHeaderToken = resolveFromAuthorizationHeader(request); final String parameterToken = isParameterTokenSupportedForRequest(request) ? resolveFromRequestParameters(request) : null; if (authorizationHeaderToken != null) { if (parameterToken != null) { final BearerTokenError error = BearerTokenErrors .invalidRequest("Found multiple bearer tokens in the request"); throw new OAuth2AuthenticationException(error); } return authorizationHeaderToken; } if (parameterToken != null && isParameterTokenEnabledForRequest(request)) { return parameterToken; } return null; } /** * Set if transport of access token using form-encoded body parameter is supported. * Defaults to {@code false}. * * @param allowFormEncodedBodyParameter if the form-encoded body parameter is * supported */ public void setAllowFormEncodedBodyParameter(boolean allowFormEncodedBodyParameter) { this.allowFormEncodedBodyParameter = allowFormEncodedBodyParameter; } /** * Set if transport of access token using URI query parameter is supported. Defaults * to {@code false}. * <p> * The spec recommends against using this mechanism for sending bearer tokens, and * even goes as far as stating that it was only included for completeness. * * @param allowUriQueryParameter if the URI query parameter is supported */ public void setAllowUriQueryParameter(boolean allowUriQueryParameter) { this.allowUriQueryParameter = allowUriQueryParameter; } /** * Set this value to configure what header is checked when resolving a Bearer Token. * This value is defaulted to {@link HttpHeaders#AUTHORIZATION}. * <p> * This allows other headers to be used as the Bearer Token source such as * {@link HttpHeaders#PROXY_AUTHORIZATION} * * @param bearerTokenHeaderName the header to check when retrieving the Bearer Token. * @since 5.4 */ public void setBearerTokenHeaderName(String bearerTokenHeaderName) { this.bearerTokenHeaderName = bearerTokenHeaderName; } private String resolveFromAuthorizationHeader(HttpServletRequest request) { String authorization = request.getHeader(this.bearerTokenHeaderName); if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) { return null; } Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization); if (!matcher.matches()) { BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed"); throw new OAuth2AuthenticationException(error); } return matcher.group("token"); } private static String resolveFromRequestParameters(HttpServletRequest request) { String[] values = request.getParameterValues("access_token"); if (values == null || values.length == 0) { return null; } if (values.length == 1) { return values[0]; } BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request"); throw new OAuth2AuthenticationException(error); } private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) { return (("POST".equals(request.getMethod()) && MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())) || "GET".equals(request.getMethod())); } private boolean isParameterTokenEnabledForRequest(final HttpServletRequest request) { return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod()) && MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType())) || (this.allowUriQueryParameter && "GET".equals(request.getMethod()))); } /** * 构建 token key * @param token token * @return token key */ private String buildKey(String token) { return String.format("%s:%s:%s", CacheConstants.TOKEN, OAuth2ParameterNames.ACCESS_TOKEN, token); } }
配置文件:
1 2 3 4 5 6 spring: security: oauth2: resourceserver: jwt: issuer-uri: http://192.168.1.186:1067
服务引用统一的资源服务器配置: pom:
1 2 3 4 <dependency> <groupId>com.luck.sugar</groupId> <artifactId>luck-common-security</artifactId> </dependency>
controller
1 2 3 4 5 6 7 8 9 10 11 @RestController public class HomeController { @PostMapping("test") @PreAuthorize("hasAuthority('ROLE_ADMIN')") public String test(){ return "test"; } }
测试: 先到授权服务器获取token
然后在资源服务器中添加key为Authorization的headers
接着在token后面加数字1
抛出异常An error occurred while attempting to decode the Jwt: Signed JWT rejected: Invalid signature
总结: 在资源服务器的流程中有多个扩展点必须的扩展点为
jwtAuthenticationConverter 他定义了权限信息从jwt的那个Claims下取出,之前的授权服务器中将权限信息放入了authorities 所以上面的的例子定义了权限信息从authorities中获取
BearerTokenResolver:提取令牌
AuthenticationManagerResolver :获取认证中心,一般使用默认的AuthenticationManager
JwtDecoder:他定义了jwt解析,可以结合jwtAuthenticationConverter 完成从缓存中获取权限信息
AuthenticationProvider:身份验证实现,如果身份验证有特殊处理可以增加
AuthenticationEntryPoint:BearerTokenResolver 获取失败的错误提示处理
AuthenticationFailureHandler: 授权失败错误处理