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進行的