module:auth
1.添加依赖:spring-cloud-starter-security与spring-cloud-starter-oauth2
2.配置WebSecurityConfig:注入AuthenticationManager覆写configure
3.配置tokenConfig
4.授权服务器配置:AuthorizationServer extends AuthorizationServerConfigurerAdapter配置客户端详细任务,令牌端点配置
5.实现UserDetailsService,覆写loadUserByUsername(最熟悉的一集)
tips:这里return 的UserDetails对象不用自己创建,springsecurity提供了一个User类实现了UserDetails,通过建造者模式可以直接生成username,password,authorities等信息。这里如果需要多参数分发给其他module使用,建议的做法是将数据库中查出来的对象通过json转成字符串存入username,后续可以直接将username反写成具体的class
module:gateway
首先明确网关的作用:路由转发、认证、白名单放行:针对当前网关的路由进行转发,如果是白名单则直接放行,如果是需要JWT校验则需要校验JWT合法性
security中提供了认证与授权,这里我们在网关进行了认证,也就是说授权模块是在各个微服务中进行的
gateway集成springsecurity:
1.添加依赖:spring-cloud-starter-security与spring-cloud-starter-oauth2
2.添加配置:过滤器;再添加tokenConfig、securityConfig
/**
*
* @description 网关认证过滤器:这段代码主要是作为过滤器去处理一个http请求,首先是检查请求路径是否在白名单中,请求是否携带token/有没有过期等安全相关检查
*/
@Component @Slf4j public class GatewayAuthFilter implements GlobalFilter, Ordered { //白名单 private static List<String> whitelist = null; static { //加载白名单 try ( InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties"); ) { Properties properties = new Properties(); properties.load(resourceAsStream); Set<String> strings = properties.stringPropertyNames(); whitelist= new ArrayList<>(strings); } catch (Exception e) { log.error("加载/security-whitelist.properties出错:{}",e.getMessage()); e.printStackTrace(); } } @Autowired private TokenStore tokenStore; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String requestUrl = exchange.getRequest().getPath().value(); AntPathMatcher pathMatcher = new AntPathMatcher(); //白名单放行 for (String url : whitelist) { if (pathMatcher.match(url, requestUrl)) { return chain.filter(exchange); } } //检查token是否存在 String token = getToken(exchange); if (StringUtils.isBlank(token)) { return buildReturnMono("没有认证",exchange); } //判断是否是有效的token OAuth2AccessToken oAuth2AccessToken; try { oAuth2AccessToken = tokenStore.readAccessToken(token); boolean expired = oAuth2AccessToken.isExpired(); if (expired) { return buildReturnMono("认证令牌已过期",exchange); } return chain.filter(exchange); } catch (InvalidTokenException e) { log.info("认证令牌无效: {}", token); return buildReturnMono("认证令牌无效",exchange); } } /** * 获取token */ private String getToken(ServerWebExchange exchange) { String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization"); if (StringUtils.isBlank(tokenStr)) { return null; } String token = tokenStr.split(" ")[1]; if (StringUtils.isBlank(token)) { return null; } return token; } private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); String jsonString = JSON.toJSONString(new RestErrorResponse(error)); byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(bits); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); return response.writeWith(Mono.just(buffer)); } @Override public int getOrder() { return 0; } }
// 假设已经从请求中获得了token String token = getToken(exchange); // 如果token存在且不为空 if (token != null && !token.isEmpty()) { // 创建一个新的ServerHttpRequest,并添加token到Authorization头 ServerHttpRequest mutatedRequest = exchange.getRequest().mutate() .header("Authorization", "Bearer " + token) .build(); // 使用ServerWebExchangeDecorator来包装原始的ServerWebExchange,并且使用修改后的请求 ServerWebExchange mutatedExchange = exchange.mutate() .request(mutatedRequest) .build(); return chain.filter(mutatedExchange); }
3.配置白名单security-whitelist.properties
/media/open/**=媒资管理公开访问接口
4.在其他微服务模块中放行所有路由,并且直接通过SecurityContextHolder.getContext().getAuthentication()获取对象(因为在网关中已经校验完了,这里只负责授权)
module:others
1.包装一个SecurityUtil。SecurityContextHolder.getContext().getAuthentication().getPrincipal();即可获得具体username,然后将username转为实体类即可使用
@Slf4j public class SecurityUtil { public static XcUser getUser() { try { Object principalObj = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principalObj instanceof String) { //取出用户身份信息 String principal = principalObj.toString(); //将json转成对象 XcUser user = JSON.parseObject(principal, XcUser.class); return user; } } catch (Exception e) { log.error("获取当前登录用户身份出错:{}", e.getMessage()); e.printStackTrace(); } return null; } @Data public static class XcUser implements Serializable { private static final long serialVersionUID = 1L; private String id; private String username; private String password; private String salt; private String name; private String nickname; private String wxUnionid; private String companyId; /** * 头像 */ private String userpic; private String utype; private LocalDateTime birthday; private String sex; private String email; private String cellphone; private String qq; /** * 用户状态 */ private String status; private LocalDateTime createTime; private LocalDateTime updateTime; } }
使用说明:上文使用了成员内部类,XcUser定义在外部类的成员位置,其不能创建独立对象,必须依靠外部类实例.new 内部类()来创建实例
SecurityUtil.XcUser user = SecurityUtil.getUser();
Q&A:
token在gateway中传递下去的?
(无oauth2集成)一般情况下我们需要修改ServerWebExchange里的ServerHttpRequest,但是ServerHttpRequest是不可变的。因此如果我们需要添加上token,就需要新建一个ServerWebExchange,重写ServerHttpRequest。
除此以外,因为我们还会将请求传递给下一个GatewayFilterChain,我们可以在这个请求传递的时候通过修改ServerWebExchange的MutableHttpRequest:
(有oauth2集成)springsecurity的过滤器链会自动处理从请求中提取token,并将其附加到上下文中,下游的过滤器会通过springsecurity的API获取他
为什么其他微服务模块能直接通过SecurityContextHolder.getContext().getAuthentication()获取对象?
在微服务中,每一个模块都有独属于自己的JVM,拥有独属于自己的线程,也就是说每个模块的SecurityContext是不一样的。当我们使用oauth2,用户通过上述gateway进行认证,并获取一个jwt将其放在request中。
当一个模块接收到一个包含oauth2令牌的http请求时,他会使用这个令牌来验证用户身份,并且基于令牌构建一个新的SecurityContext。也就是说SecurityContext的实现其实是局限于单个服务的单个线程的,跨服务的安全信息传递是由oauth2进行的