引言
原本打算將Security模塊與gateway模塊分開(kāi)寫(xiě)的,但想到gateway本來(lái)就有過(guò)濾的作用 ,于是就把gateway和Security結(jié)合在一起了,然后結(jié)合JWT令牌對(duì)用戶身份和權(quán)限進(jìn)行校驗(yàn)。
Spring Cloud的網(wǎng)關(guān)與傳統(tǒng)的SpringMVC不同,gateway是基于Netty容器,采用的webflux技術(shù),所以gateway模塊不能引入spring web包。雖然是不同,但是在SpringMVC模式下的Security實(shí)現(xiàn)步驟和流程都差不多。
依賴
Spring? cloud gateway模塊依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--JWT的依賴-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
代碼基本結(jié)構(gòu)
認(rèn)證執(zhí)行流程
一、Token工具類
public class JWTUtils {
private final static String SING="XIAOYUAN";
public static String creatToken(Map<String,String> payload,int expireTime){
JWTCreator.Builder builder= JWT.create();
Calendar instance=Calendar.getInstance();//獲取日歷對(duì)象
if(expireTime <=0)
instance.add(Calendar.SECOND,3600);//默認(rèn)一小時(shí)
else
instance.add(Calendar.SECOND,expireTime);
//為了方便只放入了一種類型
payload.forEach(builder::withClaim);
return builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SING));
}
public static Map<String, Object> getTokenInfo(String token){
DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
Map<String, Claim> claims = verify.getClaims();
SimpleDateFormat dateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String expired= dateTime.format(verify.getExpiresAt());
Map<String,Object> m=new HashMap<>();
claims.forEach((k,v)-> m.put(k,v.asString()));
m.put("exp",expired);
return m;
}
}
二、自定義User并且實(shí)現(xiàn)Spring Security的User接口,以及實(shí)現(xiàn)UserDetail接口
public class SecurityUserDetails extends User implements Serializable {
private Long userId;
public SecurityUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities, Long userId) {
super(username, password, authorities);
this.userId = userId;
}
public SecurityUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities, Long userId) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.userId = userId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
}
@Component("securityUserDetailsService")
@Slf4j
public class SecurityUserDetailsService implements ReactiveUserDetailsService {
private final PasswordEncoder passwordEncoder= new BCryptPasswordEncoder();;
@Override
public Mono<UserDetails> findByUsername(String username) {
//調(diào)用數(shù)據(jù)庫(kù)根據(jù)用戶名獲取用戶
log.info(username);
if(!username.equals("admin")&&!username.equals("user"))
throw new UsernameNotFoundException("username error");
else {
Collection<GrantedAuthority> authorities = new ArrayList<>();
if (username.equals("admin"))
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));//ROLE_ADMIN
if (username.equals("user"))
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));//ROLE_ADMIN
SecurityUserDetails securityUserDetails = new SecurityUserDetails(username,"{bcrypt}"+passwordEncoder.encode("123"),authorities,1L);
return Mono.just(securityUserDetails);
}
}
}
這里我為了方便測(cè)試,只設(shè)置了兩個(gè)用戶,admin和晢user,用戶角色也只有一種。
二、AuthenticationSuccessHandler,定義認(rèn)證成功類
@Component
@Slf4j
public class AuthenticationSuccessHandler extends WebFilterChainServerAuthenticationSuccessHandler {
@Value("${login.timeout}")
private int timeout=3600;//默認(rèn)一小時(shí)
private final int rememberMe=180;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@SneakyThrows
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
ServerWebExchange exchange = webFilterExchange.getExchange();
ServerHttpResponse response = exchange.getResponse();
//設(shè)置headers
HttpHeaders httpHeaders = response.getHeaders();
httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
//設(shè)置body
HashMap<String, String> map = new HashMap<>();
String remember_me=exchange.getRequest().getHeaders().getFirst("Remember-me");
ObjectMapper mapper = new ObjectMapper();
List<? extends GrantedAuthority> list=authentication.getAuthorities().stream().toList();
try {
Map<String, String> load = new HashMap<>();
load.put("username",authentication.getName());
load.put("role",list.get(0).getAuthority());//這里只添加了一種角色 實(shí)際上用戶可以有不同的角色類型
String token;
log.info(authentication.toString());
if (remember_me==null) {
token=JWTUtils.creatToken(load,3600*24);
response.addCookie(ResponseCookie.from("token", token).path("/").build());
//maxAge默認(rèn)-1 瀏覽器關(guān)閉cookie失效
redisTemplate.opsForValue().set(authentication.getName(), token, 1, TimeUnit.DAYS);
}else {
token=JWTUtils.creatToken(load,3600*24*180);
response.addCookie(ResponseCookie.from("token", token).maxAge(Duration.ofDays(rememberMe)).path("/").build());
redisTemplate.opsForValue().set(authentication.getName(), token, rememberMe, TimeUnit.SECONDS);//保存180天
}
map.put("code", "000220");
map.put("message", "登錄成功");
map.put("token",token);
} catch (Exception ex) {
ex.printStackTrace();
map.put("code", "000440");
map.put("message","登錄失敗");
}
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(mapper.writeValueAsBytes(map));
return response.writeWith(Mono.just(bodyDataBuffer));
}
}
當(dāng)用戶認(rèn)證成功的時(shí)候就會(huì)調(diào)用這個(gè)類,這里我將token作為cookie返回客戶端,當(dāng)客服端請(qǐng)求接口的時(shí)候?qū)螩ookie,然后gateway在認(rèn)證之前攔截,然后將Cookie寫(xiě)入Http請(qǐng)求頭中,后面的授權(quán)在請(qǐng)求頭中獲取token。(這里我使用的cookie來(lái)保存token,當(dāng)然也可以保存在localStorage里,每次請(qǐng)求的headers里面帶上token)
這里還實(shí)現(xiàn)了一個(gè)記住用戶登錄的功能,原本是打算讀取請(qǐng)求頭中的表單數(shù)據(jù)的Remember-me字段來(lái)判斷是否記住用戶登錄狀態(tài),但是這里有一個(gè)問(wèn)題,在獲取請(qǐng)求的表單數(shù)據(jù)的時(shí)候一直為空,因?yàn)閃ebflux中請(qǐng)求體中的數(shù)據(jù)只能被讀取一次,如果讀取了就需要重新封裝,前面在進(jìn)行用戶認(rèn)證的時(shí)候已經(jīng)讀取過(guò)了請(qǐng)求體導(dǎo)致后面就讀取不了(只是猜測(cè),因?yàn)閯倢W(xué)習(xí)gateway還不是很了解,在網(wǎng)上查了很多資料一直沒(méi)有解決這個(gè)問(wèn)題),于是我用了另一個(gè)方法,需要記住用戶登錄狀態(tài)的時(shí)候(Remember-me),我就在前端請(qǐng)求的時(shí)候往Http請(qǐng)求頭加一個(gè)Remember-me字段,然后后端判斷有沒(méi)有這個(gè)字段,沒(méi)有的話就不記住。
三、AuthenticationFaillHandler? ,認(rèn)證失敗類
@Slf4j
@Component
public class AuthenticationFaillHandler implements ServerAuthenticationFailureHandler {
@SneakyThrows
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException e) {
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
HashMap<String, String> map = new HashMap<>();
map.put("code", "000400");
map.put("message", e.getMessage());
log.error("access forbidden path={}", webFilterExchange.getExchange().getRequest().getPath());
ObjectMapper objectMapper = new ObjectMapper();
DataBuffer dataBuffer = response.bufferFactory().wrap(objectMapper.writeValueAsBytes(map));
return response.writeWith(Mono.just(dataBuffer));
}
}
四、SecurityRepository ,用戶信息上下文存儲(chǔ)類
@Slf4j
@Component
public class SecurityRepository implements ServerSecurityContextRepository {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
return Mono.empty();
}
@Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
String token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
log.info(token);
if (token != null) {
try {
Map<String,Object> userMap= JWTUtils.getTokenInfo(token);
String result=(String)redisTemplate.opsForValue().get(userMap.get("username"));
if (result==null || !result.equals(token))
return Mono.empty();
SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
Collection<SimpleGrantedAuthority> authorities=new ArrayList<>();
log.info((String) userMap.get("role"));
authorities.add(new SimpleGrantedAuthority((String) userMap.get("role")));
Authentication authentication=new UsernamePasswordAuthenticationToken(null, null,authorities);
emptyContext.setAuthentication(authentication);
return Mono.just(emptyContext);
}catch (Exception e) {
return Mono.empty();
}
}
return Mono.empty();
}
}
當(dāng)客戶端訪問(wèn)服務(wù)接口的時(shí)候,如果是有效token,那么就根據(jù)token來(lái)判斷用戶權(quán)限,實(shí)現(xiàn)ServerSecurityContextRepository?類的主要目的是實(shí)現(xiàn)load方法,這個(gè)方法實(shí)際上是傳遞一個(gè)Authentication對(duì)象供后面ReactiveAuthorizationManager<AuthorizationContext>來(lái)判斷用戶權(quán)限。我這里只傳遞了用戶的role信息,所以就沒(méi)有去實(shí)現(xiàn)ReactiveAuthorizationManager這個(gè)接口了。
Security框架默認(rèn)提供了兩個(gè)ServerSecurityContextRepository實(shí)現(xiàn)類,WebSessionServerSecurityContextRepository和NoOpServerSecurityContextRepository,Security默認(rèn)使用WebSessionServerSecurityContextRepository,這個(gè)是使用session來(lái)保存用戶登錄狀態(tài)的,NoOpServerSecurityContextRepository是無(wú)狀態(tài)的。
五、AuthenticationEntryPoint ,接口認(rèn)證入口類
如果客戶端沒(méi)有認(rèn)證授權(quán)就直接訪問(wèn)服務(wù)接口,然后就會(huì)調(diào)用這個(gè)類,返回的狀態(tài)碼是401
@Slf4j
@Component
public class AuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint {
@SneakyThrows
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
HashMap<String, String> map = new HashMap<>();
map.put("status", "00401");
map.put("message", "未登錄");
ObjectMapper objectMapper = new ObjectMapper();
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(objectMapper.writeValueAsBytes(map));
return response.writeWith(Mono.just(bodyDataBuffer));
}
}
六、AccessDeniedHandler ,授權(quán)失敗處理類
當(dāng)訪問(wèn)服務(wù)接口的用戶權(quán)限不夠時(shí)會(huì)調(diào)用這個(gè)類,返回HTTP狀態(tài)碼是403
@Slf4j
@Component
public class AccessDeniedHandler implements ServerAccessDeniedHandler {
@SneakyThrows
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
HashMap<String, String> map = new HashMap<>();
map.put("code", "000403");
map.put("message", "未授權(quán)禁止訪問(wèn)");
log.error("access forbidden path={}", exchange.getRequest().getPath());
ObjectMapper objectMapper = new ObjectMapper();
DataBuffer dataBuffer = response.bufferFactory().wrap(objectMapper.writeValueAsBytes(map));
return response.writeWith(Mono.just(dataBuffer));
}
}
七、AuthorizationManager ,鑒權(quán)管理類
@Slf4j
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
return authentication.map(auth -> {
//SecurityUserDetails userSecurity = (SecurityUserDetails) auth.getPrincipal();
String path=authorizationContext.getExchange().getRequest().getURI().getPath();
for (GrantedAuthority authority : auth.getAuthorities()){
if (authority.getAuthority().equals("ROLE_USER")&&path.contains("/user/normal"))
return new AuthorizationDecision(true);
else if (authority.getAuthority().equals("ROLE_ADMIN")&&path.contains("/user/admin"))
return new AuthorizationDecision(true);
//對(duì)客戶端訪問(wèn)路徑與用戶角色進(jìn)行匹配
}
return new AuthorizationDecision(false);
}).defaultIfEmpty(new AuthorizationDecision(false));
}
}
返回new AuthorizationDecision(true)代表授予權(quán)限訪問(wèn)服務(wù),為false則是拒絕。
八、LogoutHandler,LogoutSuccessHandler?登出處理類
@Component
@Slf4j
public class LogoutHandler implements ServerLogoutHandler {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Override
public Mono<Void> logout(WebFilterExchange webFilterExchange, Authentication authentication) {
HttpCookie cookie=webFilterExchange.getExchange().getRequest().getCookies().getFirst("token");
try {
if (cookie != null) {
Map<String,Object> userMap= JWTUtils.getTokenInfo(cookie.getValue());
redisTemplate.delete((String) userMap.get("username"));
}
}catch (JWTDecodeException e) {
return Mono.error(e);
}
return Mono.empty();
}
}
@Component
public class LogoutSuccessHandler implements ServerLogoutSuccessHandler {
@SneakyThrows
@Override
public Mono<Void> onLogoutSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
//設(shè)置headers
HttpHeaders httpHeaders = response.getHeaders();
httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
//設(shè)置body
HashMap<String, String> map = new HashMap<>();
//刪除token
response.addCookie(ResponseCookie.from("token", "logout").maxAge(0).path("/").build());
map.put("code", "000220");
map.put("message", "退出登錄成功");
ObjectMapper mapper = new ObjectMapper();
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(mapper.writeValueAsBytes(map));
return response.writeWith(Mono.just(bodyDataBuffer));
}
}
九、CookieToHeadersFilter ,將Cookie寫(xiě)入Http請(qǐng)求頭中
@Slf4j
@Component
public class CookieToHeadersFilter implements WebFilter{
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
try {
HttpCookie cookie=exchange.getRequest().getCookies().getFirst("token");
if (cookie != null) {
String token = cookie.getValue();
ServerHttpRequest request=exchange.getRequest().mutate().header(HttpHeaders.AUTHORIZATION,token).build();
return chain.filter(exchange.mutate().request(request).build());
}
}catch (NoFoundToken e) {
log.error(e.getMsg());
}
return chain.filter(exchange);
}
}
這里需要注意的是,如果要想在認(rèn)證前后過(guò)濾Http請(qǐng)求,用全局過(guò)濾器或者局部過(guò)濾器是不起作用的,因?yàn)樗鼈兛偸窃阼b權(quán)通過(guò)后執(zhí)行,也就是它們的執(zhí)行順序始終再Security過(guò)濾器之后,無(wú)論order值多大多小。這時(shí)候必須實(shí)現(xiàn)的接口是WebFilter而不是GlobalFilter或者GatewayFilter,然后將接口實(shí)現(xiàn)類添加到WebSecurityConfig配置中心去。
十、WebSecurityConfig,配置類
@EnableWebFluxSecurity
@Configuration
@Slf4j
public class WebSecurityConfig {
@Autowired
SecurityUserDetailsService securityUserDetailsService;
@Autowired
AuthorizationManager authorizationManager;
@Autowired
AccessDeniedHandler accessDeniedHandler;
@Autowired
AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
AuthenticationFaillHandler authenticationFaillHandler;
@Autowired
SecurityRepository securityRepository;
@Autowired
CookieToHeadersFilter cookieToHeadersFilter;
@Autowired
LogoutSuccessHandler logoutSuccessHandler;
@Autowired
LogoutHandler logoutHandler;
@Autowired
com.example.gateway.security.AuthenticationEntryPoint authenticationEntryPoint;
private final String[] path={
"/favicon.ico",
"/book/**",
"/user/login.html",
"/user/__MACOSX/**",
"/user/css/**",
"/user/fonts/**",
"/user/images/**"};
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.addFilterBefore(cookieToHeadersFilter, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
//SecurityWebFiltersOrder枚舉類定義了執(zhí)行次序
http.authorizeExchange(exchange -> exchange // 請(qǐng)求攔截處理
.pathMatchers(path).permitAll()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.anyExchange().access(authorizationManager)//權(quán)限
//.and().authorizeExchange().pathMatchers("/user/normal/**").hasRole("ROLE_USER")
//.and().authorizeExchange().pathMatchers("/user/admin/**").hasRole("ROLE_ADMIN")
//也可以這樣寫(xiě) 將匹配路徑和角色權(quán)限寫(xiě)在一起
)
.httpBasic()
.and()
.formLogin().loginPage("/user/login")//登錄接口
.authenticationSuccessHandler(authenticationSuccessHandler) //認(rèn)證成功
.authenticationFailureHandler(authenticationFaillHandler) //登陸驗(yàn)證失敗
.and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)//基于http的接口請(qǐng)求鑒權(quán)失敗
.and().csrf().disable()//必須支持跨域
.logout().logoutUrl("/user/logout")
.logoutHandler(logoutHandler)
.logoutSuccessHandler(logoutSuccessHandler);
http.securityContextRepository(securityRepository);
//http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance());//無(wú)狀態(tài) 默認(rèn)情況下使用的WebSession
return http.build();
}
@Bean
public ReactiveAuthenticationManager reactiveAuthenticationManager() {
LinkedList<ReactiveAuthenticationManager> managers = new LinkedList<>();
managers.add(authentication -> {
// 其他登陸方式
return Mono.empty();
});
managers.add(new UserDetailsRepositoryReactiveAuthenticationManager(securityUserDetailsService));
return new DelegatingReactiveAuthenticationManager(managers);
}
}
十一、測(cè)試
首先沒(méi)有登錄訪問(wèn)服務(wù)
然后登錄?
訪問(wèn)服務(wù)
訪問(wèn)另一個(gè)接口文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-779929.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-779929.html
到了這里,關(guān)于SpringCloud gateway+Spring Security + JWT實(shí)現(xiàn)登錄和用戶權(quán)限校驗(yàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!