赞
踩
应用架构:
认证服务负责认证,网关负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。
安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。
JWT认证流程:
1、用户使用账号和密码发出post请求;
2、服务器使用私钥创建一个jwt;
3、服务器返回这个jwt给浏览器;
4、浏览器将该jwt串在请求头中像服务器发送请求;
5、服务器验证该jwt;
6、返回响应的资源给浏览器。
JWT解析
JWT使用场景:
服务划分:
使用keytool生成RSA证书jwt.jks,复制到resource目录下,在JDK的bin目录下使用如下命令即可;
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
Oauth2配置类AuthorizationServerConfigurerAdapter
AuthorizationServerConfigurerAdapter中:
/** * 认证服务器配置 */ @AllArgsConstructor @Configuration @EnableAuthorizationServer public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter { private final DataSource dataSource; private final PasswordEncoder passwordEncoder; private final UserServiceImpl userDetailsService; private final AuthenticationManager authenticationManager; public static final String CLIENT_ID = "client-app"; public static final String CLIENT_SECRET = "12345678"; /* @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(CLIENT_ID) .secret(passwordEncoder.encode(CLIENT_SECRET)) .scopes("all") .authorizedGrantTypes("password", "refresh_token") .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(86400); }*/ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 从jdbc查出数据来存储 clients.withClientDetails(new JdbcClientDetailsService(dataSource)); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients(); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); enhancerChain.setTokenEnhancers(Lists.newArrayList(tokenEnhancer(), accessTokenConverter())); //配置JWT的内容增强器 endpoints .authenticationManager(authenticationManager) // 配置加载用户信息的服务 .userDetailsService(userDetailsService) // 配置JwtAccessToken转换器 .accessTokenConverter(accessTokenConverter()) .tokenEnhancer(enhancerChain); } /** * 使用非对称加密算法来对Token进行签名 */ @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setKeyPair(keyPair()); return jwtAccessTokenConverter; } @Bean public KeyPair keyPair() { // 从classpath下的证书中获取秘钥对 String password = "123456"; KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), password.toCharArray()); return keyStoreKeyFactory.getKeyPair("jwt", password.toCharArray()); } /** * 往JWT中添加自定义信息 */ @Bean public TokenEnhancer tokenEnhancer() { return (accessToken, authentication) -> { SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); Map<String, Object> info = new HashMap<>(); // 把用户ID设置到JWT中 info.put("userId", securityUser.getId()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; }; } public static void main(String[] args) { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); System.out.println(encoder.encode("12345678")); } }
网关服务需要RSA的公钥来验证签名是否合法,所以认证服务需要有个接口把公钥暴露出来;
/** * 获取RSA公钥接口 */ @RestController public class KeyPairController { @Autowired private KeyPair keyPair; @GetMapping("/rsa/publicKey") public Map<String, Object> getKey() { RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAKey key = new RSAKey.Builder(publicKey).build(); return new JWKSet(key).toJSONObject(); } }
允许获取公钥接口的访问;
/** * SpringSecurity配置 */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() .antMatchers("/rsa/publicKey").permitAll() .anyRequest().authenticated(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
创建一个资源服务ResourceServiceImpl,初始化的时候把资源与角色匹配关系缓存到Redis中,方便网关服务进行鉴权的时候获取。
/** * 资源与角色匹配关系管理业务类 */ @Service public class ResourceServiceImpl { private Map<String, List<String>> resourceRolesMap; @Autowired private RedisTemplate<String,Object> redisTemplate; @PostConstruct public void initData() { resourceRolesMap = new TreeMap<>(); resourceRolesMap.put("/api/hello", CollUtil.toList("ADMIN")); resourceRolesMap.put("/api/user/currentUser", CollUtil.toList("ADMIN", "TEST")); redisTemplate.opsForHash().putAll(RedisConstant.RESOURCE_ROLES_MAP, resourceRolesMap); } }
它将作为Oauth2的资源服务、客户端服务使用,对访问微服务的请求进行统一的校验认证和鉴权操作。
在application.yml中添加相关配置,主要是路由规则的配置、Oauth2中RSA公钥的配置及路由白名单的配置;
server: port: 9201 spring: profiles: active: dev application: name: micro-oauth2-gateway cloud: nacos: discovery: server-addr: localhost:8848 gateway: routes: #配置路由规则 - id: oauth2-api-route uri: lb://micro-oauth2-api predicates: - Path=/api/** filters: - StripPrefix=1 - id: oauth2-auth-route uri: lb://micro-oauth2-auth predicates: - Path=/auth/** filters: - StripPrefix=1 discovery: locator: enabled: true #开启从注册中心动态创建路由的功能 lower-case-service-id: true #使用小写服务名,默认是大写 security: oauth2: resourceserver: jwt: jwk-set-uri: 'http://localhost:9401/rsa/publicKey' #配置RSA的公钥访问地址 redis: database: 0 port: 6379 host: localhost password: secure: ignore: urls: #配置白名单路径 - "/actuator/**" - "/auth/oauth/token"
对网关服务进行配置安全配置,由于Gateway使用的是WebFlux,所以需要使用@EnableWebFluxSecurity注解开启;
/** * 资源服务器配置 */ @AllArgsConstructor @Configuration @EnableWebFluxSecurity public class ResourceServerConfig { private final AuthorizationManager authorizationManager; private final IgnoreUrlsConfig ignoreUrlsConfig; private final RestfulAccessDeniedHandler restfulAccessDeniedHandler; private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter; @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.oauth2ResourceServer().jwt() .jwtAuthenticationConverter(jwtAuthenticationConverter()); //自定义处理JWT请求头过期或签名错误的结果 http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint); //对白名单路径,直接移除JWT请求头 http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION); http.authorizeExchange() .pathMatchers("/css/**").permitAll() .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//白名单配置 .anyExchange().access(authorizationManager)//鉴权管理器配置 .and().exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权 .authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证 .and().csrf().disable(); return http.build(); } @Bean public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX); jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); } }
在WebFluxSecurity中自定义鉴权操作需要实现ReactiveAuthorizationManager接口;
/** * 鉴权管理器,用于判断是否有资源的访问权限 */ @Component public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) { //从Redis中获取当前路径可访问角色列表 URI uri = authorizationContext.getExchange().getRequest().getURI(); Object obj = redisTemplate.opsForHash().get(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath()); List<String> authorities = Convert.toList(String.class, obj); authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList()); //认证通过且角色匹配的用户可访问当前路径 return mono .filter(Authentication::isAuthenticated) .flatMapIterable(Authentication::getAuthorities) .map(GrantedAuthority::getAuthority) //.any(authorities::contains) // 不需要角色就可以访问 .any(role -> true) .map(AuthorizationDecision::new) .defaultIfEmpty(new AuthorizationDecision(false)); } }
这里我们还需要实现一个全局过滤器AuthGlobalFilter,当鉴权通过后将JWT令牌中的用户信息解析出来,然后存入请求的Header中,这样后续服务就不需要解析JWT令牌了,可以直接从请求的Header中获取到用户信息。
/** * 将登录用户的JWT转化成用户信息的全局过滤器 */ @Component public class AuthGlobalFilter implements GlobalFilter, Ordered { private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (StrUtil.isEmpty(token)) { return chain.filter(exchange); } try { String realToken = token.replace("Bearer ", ""); JWSObject jwsObject = JWSObject.parse(realToken); String userStr = jwsObject.getPayload().toString(); LOGGER.info("AuthGlobalFilter.filter() user:{}",userStr); // 从token中解析用户信息并设置到Header中去 ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStr).build(); exchange = exchange.mutate().request(request).build(); } catch (ParseException e) { e.printStackTrace(); } return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
它不会集成和实现任何安全相关逻辑,全靠网关来保护它。
创建一个测试接口,网关验证通过即可访问;
@GetMapping("/demo")
public String demo(@RequestHeader("user") String user) {
return "Hello demo." + user;
}
创建一个LoginUserHolder组件,用于从请求的Header中直接获取登录用户信息;
/** * 获取登录用户信息 */ @Component public class LoginUserHolder { public UserDTO getCurrentUser() { //从Header中获取用户信息 ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = servletRequestAttributes.getRequest(); String userStr = request.getHeader("user"); JSONObject userJsonObject = new JSONObject(userStr); UserDTO userDTO = new UserDTO(); userDTO.setUsername(userJsonObject.getStr("user_name")); userDTO.setId(Convert.toLong(userJsonObject.get("userId"))); userDTO.setRoles(Convert.toList(String.class, userJsonObject.get("authorities"))); return userDTO; } }
微服务系统中的统一认证鉴权功能,所有请求均通过网关访问。
使用密码模式获取JWT令牌,访问地址:http://localhost:9201/auth/oauth/token
post表单模式请求。
grant_type:password
client_id:client-app
client_secret:12345678
username:macro
password:12345678
使用获取到的JWT令牌访问需要权限的接口,访问地址:http://localhost:9201/api/demo
Bearer
当JWT令牌过期时,使用refresh_token获取新的JWT令牌,访问地址:http://localhost:9201/auth/oauth/token
grant_type:refresh_token
client_id:client-app
client_secret:12345678
refresh_token:eyJhbGciOiJSUzI1NiIsInR5…
https://gitee.com/yyangqqian/micro-oauth2.git
参考:
三分钟带你了解JWT认证
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。