素材巴巴 > 程序开发 >

SpringCloud gateway Spring Security JWT实现登录和用户权限校验

程序开发 2023-09-03 11:15:25

引言

原本打算将Security模块与gateway模块分开写的,但想到gateway本来就有过滤的作用 ,于是就把gateway和Security结合在一起了,然后结合JWT令牌对用户身份和权限进行校验。

Spring Cloud的网关与传统的SpringMVC不同,gateway是基于Netty容器,采用的webflux技术,所以gateway模块不能引入spring web包。虽然是不同,但是在SpringMVC模式下的Security实现步骤和流程都差不多。

依赖

Spring  cloud gateway模块依赖

org.springframework.cloudspring-cloud-starterorg.springframework.cloudspring-cloud-starter-gatewayorg.springframework.cloudspring-cloud-starter-netflix-eureka-clientcom.auth0java-jwt3.4.0com.fasterxml.jackson.datatypejackson-datatype-jsr310org.springframework.bootspring-boot-starter-data-redisredis.clientsjedisjarorg.springframework.dataspring-data-redis

代码基本结构

认证执行流程

一、Token工具类

public class JWTUtils {private final static String SING="XIAOYUAN";public static String creatToken(Map payload,int expireTime){JWTCreator.Builder builder= JWT.create();Calendar instance=Calendar.getInstance();//获取日历对象if(expireTime <=0)instance.add(Calendar.SECOND,3600);//默认一小时elseinstance.add(Calendar.SECOND,expireTime);//为了方便只放入了一种类型payload.forEach(builder::withClaim);return builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SING));}public static Map getTokenInfo(String token){DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token);Map claims = verify.getClaims();SimpleDateFormat dateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String expired= dateTime.format(verify.getExpiresAt());Map m=new HashMap<>();claims.forEach((k,v)-> m.put(k,v.asString()));m.put("exp",expired);return m;}
 }

二、自定义User并且实现Spring Security的User接口,以及实现UserDetail接口

 
 
public class SecurityUserDetails extends User implements Serializable {private Long userId;public SecurityUserDetails(String username, String password, Collection 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 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();;@Overridepublic Mono findByUsername(String username) {//调用数据库根据用户名获取用户log.info(username);if(!username.equals("admin")&&!username.equals("user"))throw new UsernameNotFoundException("username error");else {Collection authorities = new ArrayList<>();if (username.equals("admin"))authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));//ROLE_ADMINif (username.equals("user"))authorities.add(new SimpleGrantedAuthority("ROLE_USER"));//ROLE_ADMINSecurityUserDetails securityUserDetails = new SecurityUserDetails(username,"{bcrypt}"+passwordEncoder.encode("123"),authorities,1L);return Mono.just(securityUserDetails);}}
 }
 

这里我为了方便测试,只设置了两个用户,admin和晢user,用户角色也只有一种。

二、AuthenticationSuccessHandler,定义认证成功类

@Component
 @Slf4j
 public class AuthenticationSuccessHandler extends WebFilterChainServerAuthenticationSuccessHandler {@Value("${login.timeout}")private int timeout=3600;//默认一小时private final int rememberMe=180;@Autowiredprivate RedisTemplate redisTemplate;@SneakyThrows@Overridepublic Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {ServerWebExchange exchange = webFilterExchange.getExchange();ServerHttpResponse response = exchange.getResponse();//设置headersHttpHeaders 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");//设置bodyHashMap map = new HashMap<>();String remember_me=exchange.getRequest().getHeaders().getFirst("Remember-me");ObjectMapper mapper = new ObjectMapper();List list=authentication.getAuthorities().stream().toList();try {Map load = new HashMap<>();load.put("username",authentication.getName());load.put("role",list.get(0).getAuthority());//这里只添加了一种角色 实际上用户可以有不同的角色类型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默认-1 浏览器关闭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));}}
 

当用户认证成功的时候就会调用这个类,这里我将token作为cookie返回客户端,当客服端请求接口的时候将带上Cookie,然后gateway在认证之前拦截,然后将Cookie写入Http请求头中,后面的授权在请求头中获取token。(这里我使用的cookie来保存token,当然也可以保存在localStorage里,每次请求的headers里面带上token)

这里还实现了一个记住用户登录的功能,原本是打算读取请求头中的表单数据的Remember-me字段来判断是否记住用户登录状态,但是这里有一个问题,在获取请求的表单数据的时候一直为空,因为Webflux中请求体中的数据只能被读取一次,如果读取了就需要重新封装,前面在进行用户认证的时候已经读取过了请求体导致后面就读取不了(只是猜测,因为刚学习gateway还不是很了解,在网上查了很多资料一直没有解决这个问题),于是我用了另一个方法,需要记住用户登录状态的时候(Remember-me),我就在前端请求的时候往Http请求头加一个Remember-me字段,然后后端判断有没有这个字段,没有的话就不记住。

三、AuthenticationFaillHandler  ,认证失败类

@Slf4j
 @Component
 public class AuthenticationFaillHandler  implements ServerAuthenticationFailureHandler {@SneakyThrows@Overridepublic Mono 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 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 ,用户信息上下文存储类

@Slf4j
 @Component
 public class SecurityRepository implements ServerSecurityContextRepository {@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic Mono save(ServerWebExchange exchange, SecurityContext context) {return Mono.empty();}@Overridepublic Mono load(ServerWebExchange exchange) {String token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);log.info(token);if (token != null) {try {Map 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 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();}
 }
 

当客户端访问服务接口的时候,如果是有效token,那么就根据token来判断用户权限,实现ServerSecurityContextRepository 类的主要目的是实现load方法,这个方法实际上是传递一个Authentication对象供后面ReactiveAuthorizationManager来判断用户权限。我这里只传递了用户的role信息,所以就没有去实现ReactiveAuthorizationManager这个接口了。

Security框架默认提供了两个ServerSecurityContextRepository实现类,WebSessionServerSecurityContextRepository和NoOpServerSecurityContextRepository,Security默认使用WebSessionServerSecurityContextRepository,这个是使用session来保存用户登录状态的,NoOpServerSecurityContextRepository是无状态的。

五、AuthenticationEntryPoint ,接口认证入口类

如果客户端没有认证授权就直接访问服务接口,然后就会调用这个类,返回的状态码是401


 @Slf4j
 @Component
 public class AuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint {@SneakyThrows@Overridepublic Mono commence(ServerWebExchange exchange, AuthenticationException e) {ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.UNAUTHORIZED);response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");HashMap 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 ,授权失败处理类

当访问服务接口的用户权限不够时会调用这个类,返回HTTP状态码是403

@Slf4j
 @Component
 public class AccessDeniedHandler implements ServerAccessDeniedHandler {@SneakyThrows@Overridepublic Mono handle(ServerWebExchange exchange, AccessDeniedException denied) {ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.FORBIDDEN);response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");HashMap map = new HashMap<>();map.put("code", "000403");map.put("message", "未授权禁止访问");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 ,鉴权管理类

@Slf4j
 @Component
 public class AuthorizationManager implements ReactiveAuthorizationManager {@Overridepublic Mono check(Mono 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);
 //对客户端访问路径与用户角色进行匹配}return new AuthorizationDecision(false);}).defaultIfEmpty(new AuthorizationDecision(false));}
 }

返回new AuthorizationDecision(true)代表授予权限访问服务,为false则是拒绝。

八、LogoutHandler,LogoutSuccessHandler 登出处理类

@Component
 @Slf4j
 public class LogoutHandler implements ServerLogoutHandler {@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic Mono logout(WebFilterExchange webFilterExchange, Authentication authentication) {HttpCookie cookie=webFilterExchange.getExchange().getRequest().getCookies().getFirst("token");try {if (cookie != null) {Map 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@Overridepublic Mono onLogoutSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {ServerHttpResponse response = webFilterExchange.getExchange().getResponse();//设置headersHttpHeaders 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");//设置bodyHashMap map = new HashMap<>();//删除tokenresponse.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写入Http请求头中

@Slf4j
 @Component
 public class CookieToHeadersFilter implements WebFilter{@Overridepublic Mono 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);}}
 

这里需要注意的是,如果要想在认证前后过滤Http请求,用全局过滤器或者局部过滤器是不起作用的,因为它们总是在鉴权通过后执行,也就是它们的执行顺序始终再Security过滤器之后,无论order值多大多小。这时候必须实现的接口是WebFilter而不是GlobalFilter或者GatewayFilter,然后将接口实现类添加到WebSecurityConfig配置中心去。

十、WebSecurityConfig,配置类

@EnableWebFluxSecurity
 @Configuration
 @Slf4j
 public class WebSecurityConfig {@AutowiredSecurityUserDetailsService securityUserDetailsService;@AutowiredAuthorizationManager authorizationManager;@AutowiredAccessDeniedHandler accessDeniedHandler;@AutowiredAuthenticationSuccessHandler authenticationSuccessHandler;@AutowiredAuthenticationFaillHandler authenticationFaillHandler;@AutowiredSecurityRepository securityRepository;@AutowiredCookieToHeadersFilter cookieToHeadersFilter;@AutowiredLogoutSuccessHandler logoutSuccessHandler;@AutowiredLogoutHandler logoutHandler;@Autowiredcom.example.gateway.security.AuthenticationEntryPoint authenticationEntryPoint;private final String[] path={"/favicon.ico","/book/**","/user/login.html","/user/__MACOSX/**","/user/css/**","/user/fonts/**","/user/images/**"};@Beanpublic SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {http.addFilterBefore(cookieToHeadersFilter, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);//SecurityWebFiltersOrder枚举类定义了执行次序http.authorizeExchange(exchange -> exchange // 请求拦截处理.pathMatchers(path).permitAll().pathMatchers(HttpMethod.OPTIONS).permitAll().anyExchange().access(authorizationManager)//权限//.and().authorizeExchange().pathMatchers("/user/normal/**").hasRole("ROLE_USER")//.and().authorizeExchange().pathMatchers("/user/admin/**").hasRole("ROLE_ADMIN")//也可以这样写 将匹配路径和角色权限写在一起).httpBasic().and().formLogin().loginPage("/user/login")//登录接口.authenticationSuccessHandler(authenticationSuccessHandler) //认证成功.authenticationFailureHandler(authenticationFaillHandler) //登陆验证失败.and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler)//基于http的接口请求鉴权失败.and().csrf().disable()//必须支持跨域.logout().logoutUrl("/user/logout").logoutHandler(logoutHandler).logoutSuccessHandler(logoutSuccessHandler);http.securityContextRepository(securityRepository);//http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance());//无状态 默认情况下使用的WebSessionreturn http.build();}@Beanpublic ReactiveAuthenticationManager reactiveAuthenticationManager() {LinkedList managers = new LinkedList<>();managers.add(authentication -> {// 其他登陆方式return Mono.empty();});managers.add(new UserDetailsRepositoryReactiveAuthenticationManager(securityUserDetailsService));return new DelegatingReactiveAuthenticationManager(managers);}}

十一、测试

首先没有登录访问服务

然后登录 

访问服务

访问另一个接口


标签:

素材巴巴 Copyright © 2013-2021 http://www.sucaibaba.com/. Some Rights Reserved. 备案号:备案中。