我们首先来搭建认证服务,它将作为Oauth2的认证服务使用,并且网关服务的鉴权功能也需要依赖它。
org.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-securityorg.springframework.cloud spring-cloud-starter-oauth2com.nimbusds nimbus-jose-jwt8.16 org.springframework.boot spring-boot-starter-data-redis
server: port: 9401 spring: profiles: active: dev application: name: micro-oauth2-auth cloud: nacos: discovery: server-addr: localhost:8848 jackson: date-format: yyyy-MM-dd HH:mm:ss redis: database: 0 port: 6379 host: localhost password: management: endpoints: web: exposure: include: "*"
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
/** * 用户管理业务类 * Created by macro on 2020/6/19. */ @Service public class UserServiceImpl implements UserDetailsService { private ListuserList; @Autowired private PasswordEncoder passwordEncoder; @PostConstruct public void initData() { String password = passwordEncoder.encode("123456"); userList = new ArrayList<>(); userList.add(new UserDTO(1L,"macro", password,1, CollUtil.toList("ADMIN"))); userList.add(new UserDTO(2L,"andy", password,1, CollUtil.toList("TEST"))); } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { List findUserList = userList.stream().filter(item -> item.getUsername().equals(username)).collect(Collectors.toList()); if (CollUtil.isEmpty(findUserList)) { throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR); } SecurityUser securityUser = new SecurityUser(findUserList.get(0)); if (!securityUser.isEnabled()) { throw new DisabledException(MessageConstant.ACCOUNT_DISABLED); } else if (!securityUser.isAccountNonLocked()) { throw new LockedException(MessageConstant.ACCOUNT_LOCKED); } else if (!securityUser.isAccountNonExpired()) { throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED); } else if (!securityUser.isCredentialsNonExpired()) { throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED); } return securityUser; } }
/** * 认证服务器配置 * Created by macro on 2020/6/19. */ @AllArgsConstructor @Configuration @EnableAuthorizationServer public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter { private final PasswordEncoder passwordEncoder; private final UserServiceImpl userDetailsService; private final AuthenticationManager authenticationManager; private final JwtTokenEnhancer jwtTokenEnhancer; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client-app") .secret(passwordEncoder.encode("123456")) .scopes("all") .authorizedGrantTypes("password", "refresh_token") .accessTokenValiditySeconds(3600) .refreshTokenValiditySeconds(86400); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); Listdelegates = new ArrayList<>(); delegates.add(jwtTokenEnhancer); delegates.add(accessTokenConverter()); enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器 endpoints.authenticationManager(authenticationManager) .userDetailsService(userDetailsService) //配置加载用户信息的服务 .accessTokenConverter(accessTokenConverter()) .tokenEnhancer(enhancerChain); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients(); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setKeyPair(keyPair()); return jwtAccessTokenConverter; } @Bean public KeyPair keyPair() { //从classpath下的证书中获取秘钥对 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray()); return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray()); } }
/** * JWT内容增强器 * Created by macro on 2020/6/19. */ @Component public class JwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); Mapinfo = new HashMap<>(); //把用户ID设置到JWT中 info.put("id", securityUser.getId()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; } }
/** * 获取RSA公钥接口 * Created by macro on 2020/6/19. */ @RestController public class KeyPairController { @Autowired private KeyPair keyPair; @GetMapping("/rsa/publicKey") public MapgetKey() { RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAKey key = new RSAKey.Builder(publicKey).build(); return new JWKSet(key).toJSONObject(); } }
/** * SpringSecurity配置 * Created by macro on 2020/6/19. */ @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(); } }
/** * 资源与角色匹配关系管理业务类 * Created by macro on 2020/6/19. */ @Service public class ResourceServiceImpl { private Map> resourceRolesMap; @Autowired private RedisTemplate 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的资源服务、客户端服务使用,对访问微服务的请求进行统一的校验认证和鉴权操作。
org.springframework.boot spring-boot-starter-webfluxorg.springframework.cloud spring-cloud-starter-gatewayorg.springframework.security spring-security-configorg.springframework.security spring-security-oauth2-resource-serverorg.springframework.security spring-security-oauth2-clientorg.springframework.security spring-security-oauth2-josecom.nimbusds nimbus-jose-jwt8.16
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"
/** * 资源服务器配置 * Created by macro on 2020/6/19. */ @AllArgsConstructor @Configuration @EnableWebFluxSecurity public class ResourceServerConfig { private final AuthorizationManager authorizationManager; private final IgnoreUrlsConfig ignoreUrlsConfig; private final RestfulAccessDeniedHandler restfulAccessDeniedHandler; private final RestAuthenticationEntryPoint restAuthenticationEntryPoint; @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.oauth2ResourceServer().jwt() .jwtAuthenticationConverter(jwtAuthenticationConverter()); http.authorizeExchange() .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> 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); } }
/** * 鉴权管理器,用于判断是否有资源的访问权限 * Created by macro on 2020/6/19. */ @Component public class AuthorizationManager implements ReactiveAuthorizationManager { @Autowired private RedisTemplateredisTemplate; @Override public Mono check(Mono mono, AuthorizationContext authorizationContext) { //从Redis中获取当前路径可访问角色列表 URI uri = authorizationContext.getExchange().getRequest().getURI(); Object obj = redisTemplate.opsForHash().get(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath()); List 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) .map(AuthorizationDecision::new) .defaultIfEmpty(new AuthorizationDecision(false)); } }
/** * 将登录用户的JWT转化成用户信息的全局过滤器 * Created by macro on 2020/6/17. */ @Component public class AuthGlobalFilter implements GlobalFilter, Ordered { private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class); @Override public Monofilter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("Authorization"); if (StrUtil.isEmpty(token)) { return chain.filter(exchange); } try { //从token中解析用户信息并设置到Header中去 String realToken = token.replace("Bearer ", ""); JWSObject jwsObject = JWSObject.parse(realToken); String userStr = jwsObject.getPayload().toString(); LOGGER.info("AuthGlobalFilter.filter() user:{}",userStr); 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; } }
最后我们搭建一个API服务,它不会集成和实现任何安全相关逻辑,全靠网关来保护它。
org.springframework.boot spring-boot-starter-web
server: port: 9501 spring: profiles: active: dev application: name: micro-oauth2-api cloud: nacos: discovery: server-addr: localhost:8848 management: endpoints: web: exposure: include: "*"
/** * 测试接口 * Created by macro on 2020/6/19. */ @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello World."; } }
/** * 获取登录用户信息 * Created by macro on 2020/6/17. */ @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("id"))); userDTO.setRoles(Convert.toList(String.class,userJsonObject.get("authorities"))); return userDTO; } }
/** * 获取登录用户信息接口 * Created by macro on 2020/6/19. */ @RestController @RequestMapping("/user") public class UserController{ @Autowired private LoginUserHolder loginUserHolder; @GetMapping("/currentUser") public UserDTO currentUser() { return loginUserHolder.getCurrentUser(); } }
接下来我们来演示下微服务系统中的统一认证鉴权功能,所有请求均通过网关访问。
https://github.com/macrozheng/springcloud-learning/tree/master/micro-oauth2