应用系统定制开发SpringCloud gateway+Spring Security + JWT实现登录和用户权限校验

引言

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

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

依赖

Spring  cloud gateway模块依赖

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.cloud</groupId>
  7. <artifactId>spring-cloud-starter-gateway</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.springframework.cloud</groupId>
  11. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  12. </dependency>
  13. <!--JWT的依赖-->
  14. <dependency>
  15. <groupId>com.auth0</groupId>
  16. <artifactId>java-jwt</artifactId>
  17. <version>3.4.0</version>
  18. </dependency>
  19. <dependency>
  20. <groupId>com.fasterxml.jackson.datatype</groupId>
  21. <artifactId>jackson-datatype-jsr310</artifactId>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-data-redis</artifactId>
  26. </dependency>
  27. <dependency>
  28. <groupId>redis.clients</groupId>
  29. <artifactId>jedis</artifactId>
  30. <type>jar</type>
  31. </dependency>
  32. <dependency>
  33. <groupId>org.springframework.data</groupId>
  34. <artifactId>spring-data-redis</artifactId>
  35. </dependency>

代码基本结构

认证执行流程

一、Token工具类

  1. public class JWTUtils {
  2. private final static String SING="XIAOYUAN";
  3. public static String creatToken(Map<String,String> payload,int expireTime){
  4. JWTCreator.Builder builder= JWT.create();
  5. Calendar instance=Calendar.getInstance();//获取日历对象
  6. if(expireTime <=0)
  7. instance.add(Calendar.SECOND,3600);//默认一小时
  8. else
  9. instance.add(Calendar.SECOND,expireTime);
  10. //为了方便只放入了一种类型
  11. payload.forEach(builder::withClaim);
  12. return builder.withExpiresAt(instance.getTime()).sign(Algorithm.HMAC256(SING));
  13. }
  14. public static Map<String, Object> getTokenInfo(String token){
  15. DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
  16. Map<String, Claim> claims = verify.getClaims();
  17. SimpleDateFormat dateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  18. String expired= dateTime.format(verify.getExpiresAt());
  19. Map<String,Object> m=new HashMap<>();
  20. claims.forEach((k,v)-> m.put(k,v.asString()));
  21. m.put("exp",expired);
  22. return m;
  23. }
  24. }

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

 
  1. public class SecurityUserDetails extends User implements Serializable {
  2. private Long userId;
  3. public SecurityUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities, Long userId) {
  4. super(username, password, authorities);
  5. this.userId = userId;
  6. }
  7. public SecurityUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities, Long userId) {
  8. super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
  9. this.userId = userId;
  10. }
  11. public Long getUserId() {
  12. return userId;
  13. }
  14. public void setUserId(Long userId) {
  15. this.userId = userId;
  16. }
  17. }
  1. @Component("securityUserDetailsService")
  2. @Slf4j
  3. public class SecurityUserDetailsService implements ReactiveUserDetailsService {
  4. private final PasswordEncoder passwordEncoder= new BCryptPasswordEncoder();;
  5. @Override
  6. public Mono<UserDetails> findByUsername(String username) {
  7. //调用数据库根据用户名获取用户
  8. log.info(username);
  9. if(!username.equals("admin")&&!username.equals("user"))
  10. throw new UsernameNotFoundException("username error");
  11. else {
  12. Collection<GrantedAuthority> authorities = new ArrayList<>();
  13. if (username.equals("admin"))
  14. authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));//ROLE_ADMIN
  15. if (username.equals("user"))
  16. authorities.add(new SimpleGrantedAuthority("ROLE_USER"));//ROLE_ADMIN
  17. SecurityUserDetails securityUserDetails = new SecurityUserDetails(username,"{bcrypt}"+passwordEncoder.encode("123"),authorities,1L);
  18. return Mono.just(securityUserDetails);
  19. }
  20. }
  21. }

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

二、AuthenticationSuccessHandler,定义认证成功类

  1. @Component
  2. @Slf4j
  3. public class AuthenticationSuccessHandler extends WebFilterChainServerAuthenticationSuccessHandler {
  4. @Value("${login.timeout}")
  5. private int timeout=3600;//默认一小时
  6. private final int rememberMe=180;
  7. @Autowired
  8. private RedisTemplate<String, Object> redisTemplate;
  9. @SneakyThrows
  10. @Override
  11. public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
  12. ServerWebExchange exchange = webFilterExchange.getExchange();
  13. ServerHttpResponse response = exchange.getResponse();
  14. //设置headers
  15. HttpHeaders httpHeaders = response.getHeaders();
  16. httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
  17. httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
  18. //设置body
  19. HashMap<String, String> map = new HashMap<>();
  20. String remember_me=exchange.getRequest().getHeaders().getFirst("Remember-me");
  21. ObjectMapper mapper = new ObjectMapper();
  22. List<? extends GrantedAuthority> list=authentication.getAuthorities().stream().toList();
  23. try {
  24. Map<String, String> load = new HashMap<>();
  25. load.put("username",authentication.getName());
  26. load.put("role",list.get(0).getAuthority());//这里只添加了一种角色 实际上用户可以有不同的角色类型
  27. String token;
  28. log.info(authentication.toString());
  29. if (remember_me==null) {
  30. token=JWTUtils.creatToken(load,3600*24);
  31. response.addCookie(ResponseCookie.from("token", token).path("/").build());
  32. //maxAge默认-1 浏览器关闭cookie失效
  33. redisTemplate.opsForValue().set(authentication.getName(), token, 1, TimeUnit.DAYS);
  34. }else {
  35. token=JWTUtils.creatToken(load,3600*24*180);
  36. response.addCookie(ResponseCookie.from("token", token).maxAge(Duration.ofDays(rememberMe)).path("/").build());
  37. redisTemplate.opsForValue().set(authentication.getName(), token, rememberMe, TimeUnit.SECONDS);//保存180天
  38. }
  39. map.put("code", "000220");
  40. map.put("message", "登录成功");
  41. map.put("token",token);
  42. } catch (Exception ex) {
  43. ex.printStackTrace();
  44. map.put("code", "000440");
  45. map.put("message","登录失败");
  46. }
  47. DataBuffer bodyDataBuffer = response.bufferFactory().wrap(mapper.writeValueAsBytes(map));
  48. return response.writeWith(Mono.just(bodyDataBuffer));
  49. }
  50. }

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

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

三、AuthenticationFaillHandler  ,认证失败类

  1. @Slf4j
  2. @Component
  3. public class AuthenticationFaillHandler implements ServerAuthenticationFailureHandler {
  4. @SneakyThrows
  5. @Override
  6. public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException e) {
  7. ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
  8. response.setStatusCode(HttpStatus.FORBIDDEN);
  9. response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
  10. HashMap<String, String> map = new HashMap<>();
  11. map.put("code", "000400");
  12. map.put("message", e.getMessage());
  13. log.error("access forbidden path={}", webFilterExchange.getExchange().getRequest().getPath());
  14. ObjectMapper objectMapper = new ObjectMapper();
  15. DataBuffer dataBuffer = response.bufferFactory().wrap(objectMapper.writeValueAsBytes(map));
  16. return response.writeWith(Mono.just(dataBuffer));
  17. }
  18. }

四、SecurityRepository ,用户信息上下文存储类

  1. @Slf4j
  2. @Component
  3. public class SecurityRepository implements ServerSecurityContextRepository {
  4. @Autowired
  5. private RedisTemplate<String, Object> redisTemplate;
  6. @Override
  7. public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
  8. return Mono.empty();
  9. }
  10. @Override
  11. public Mono<SecurityContext> load(ServerWebExchange exchange) {
  12. String token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
  13. log.info(token);
  14. if (token != null) {
  15. try {
  16. Map<String,Object> userMap= JWTUtils.getTokenInfo(token);
  17. String result=(String)redisTemplate.opsForValue().get(userMap.get("username"));
  18. if (result==null || !result.equals(token))
  19. return Mono.empty();
  20. SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();
  21. Collection<SimpleGrantedAuthority> authorities=new ArrayList<>();
  22. log.info((String) userMap.get("role"));
  23. authorities.add(new SimpleGrantedAuthority((String) userMap.get("role")));
  24. Authentication authentication=new UsernamePasswordAuthenticationToken(null, null,authorities);
  25. emptyContext.setAuthentication(authentication);
  26. return Mono.just(emptyContext);
  27. }catch (Exception e) {
  28. return Mono.empty();
  29. }
  30. }
  31. return Mono.empty();
  32. }
  33. }

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

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

五、AuthenticationEntryPoint ,接口认证入口类

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

  1. @Slf4j
  2. @Component
  3. public class AuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint {
  4. @SneakyThrows
  5. @Override
  6. public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
  7. ServerHttpResponse response = exchange.getResponse();
  8. response.setStatusCode(HttpStatus.UNAUTHORIZED);
  9. response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
  10. HashMap<String, String> map = new HashMap<>();
  11. map.put("status", "00401");
  12. map.put("message", "未登录");
  13. ObjectMapper objectMapper = new ObjectMapper();
  14. DataBuffer bodyDataBuffer = response.bufferFactory().wrap(objectMapper.writeValueAsBytes(map));
  15. return response.writeWith(Mono.just(bodyDataBuffer));
  16. }
  17. }

六、AccessDeniedHandler ,授权失败处理类

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

  1. @Slf4j
  2. @Component
  3. public class AccessDeniedHandler implements ServerAccessDeniedHandler {
  4. @SneakyThrows
  5. @Override
  6. public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
  7. ServerHttpResponse response = exchange.getResponse();
  8. response.setStatusCode(HttpStatus.FORBIDDEN);
  9. response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
  10. HashMap<String, String> map = new HashMap<>();
  11. map.put("code", "000403");
  12. map.put("message", "未授权禁止访问");
  13. log.error("access forbidden path={}", exchange.getRequest().getPath());
  14. ObjectMapper objectMapper = new ObjectMapper();
  15. DataBuffer dataBuffer = response.bufferFactory().wrap(objectMapper.writeValueAsBytes(map));
  16. return response.writeWith(Mono.just(dataBuffer));
  17. }
  18. }

七、AuthorizationManager ,鉴权管理类

  1. @Slf4j
  2. @Component
  3. public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
  4. @Override
  5. public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
  6. return authentication.map(auth -> {
  7. //SecurityUserDetails userSecurity = (SecurityUserDetails) auth.getPrincipal();
  8. String path=authorizationContext.getExchange().getRequest().getURI().getPath();
  9. for (GrantedAuthority authority : auth.getAuthorities()){
  10. if (authority.getAuthority().equals("ROLE_USER")&&path.contains("/user/normal"))
  11. return new AuthorizationDecision(true);
  12. else if (authority.getAuthority().equals("ROLE_ADMIN")&&path.contains("/user/admin"))
  13. return new AuthorizationDecision(true);
  14. //对客户端访问路径与用户角色进行匹配
  15. }
  16. return new AuthorizationDecision(false);
  17. }).defaultIfEmpty(new AuthorizationDecision(false));
  18. }
  19. }

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

八、LogoutHandler,LogoutSuccessHandler 登出处理类

  1. @Component
  2. @Slf4j
  3. public class LogoutHandler implements ServerLogoutHandler {
  4. @Autowired
  5. private RedisTemplate<String,Object> redisTemplate;
  6. @Override
  7. public Mono<Void> logout(WebFilterExchange webFilterExchange, Authentication authentication) {
  8. HttpCookie cookie=webFilterExchange.getExchange().getRequest().getCookies().getFirst("token");
  9. try {
  10. if (cookie != null) {
  11. Map<String,Object> userMap= JWTUtils.getTokenInfo(cookie.getValue());
  12. redisTemplate.delete((String) userMap.get("username"));
  13. }
  14. }catch (JWTDecodeException e) {
  15. return Mono.error(e);
  16. }
  17. return Mono.empty();
  18. }
  19. }
  1. @Component
  2. public class LogoutSuccessHandler implements ServerLogoutSuccessHandler {
  3. @SneakyThrows
  4. @Override
  5. public Mono<Void> onLogoutSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
  6. ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
  7. //设置headers
  8. HttpHeaders httpHeaders = response.getHeaders();
  9. httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
  10. httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
  11. //设置body
  12. HashMap<String, String> map = new HashMap<>();
  13. //删除token
  14. response.addCookie(ResponseCookie.from("token", "logout").maxAge(0).path("/").build());
  15. map.put("code", "000220");
  16. map.put("message", "退出登录成功");
  17. ObjectMapper mapper = new ObjectMapper();
  18. DataBuffer bodyDataBuffer = response.bufferFactory().wrap(mapper.writeValueAsBytes(map));
  19. return response.writeWith(Mono.just(bodyDataBuffer));
  20. }
  21. }

九、CookieToHeadersFilter ,将Cookie写入Http请求头中

  1. @Slf4j
  2. @Component
  3. public class CookieToHeadersFilter implements WebFilter{
  4. @Override
  5. public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
  6. try {
  7. HttpCookie cookie=exchange.getRequest().getCookies().getFirst("token");
  8. if (cookie != null) {
  9. String token = cookie.getValue();
  10. ServerHttpRequest request=exchange.getRequest().mutate().header(HttpHeaders.AUTHORIZATION,token).build();
  11. return chain.filter(exchange.mutate().request(request).build());
  12. }
  13. }catch (NoFoundToken e) {
  14. log.error(e.getMsg());
  15. }
  16. return chain.filter(exchange);
  17. }
  18. }

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

十、WebSecurityConfig,配置类

  1. @EnableWebFluxSecurity
  2. @Configuration
  3. @Slf4j
  4. public class WebSecurityConfig {
  5. @Autowired
  6. SecurityUserDetailsService securityUserDetailsService;
  7. @Autowired
  8. AuthorizationManager authorizationManager;
  9. @Autowired
  10. AccessDeniedHandler accessDeniedHandler;
  11. @Autowired
  12. AuthenticationSuccessHandler authenticationSuccessHandler;
  13. @Autowired
  14. AuthenticationFaillHandler authenticationFaillHandler;
  15. @Autowired
  16. SecurityRepository securityRepository;
  17. @Autowired
  18. CookieToHeadersFilter cookieToHeadersFilter;
  19. @Autowired
  20. LogoutSuccessHandler logoutSuccessHandler;
  21. @Autowired
  22. LogoutHandler logoutHandler;
  23. @Autowired
  24. com.example.gateway.security.AuthenticationEntryPoint authenticationEntryPoint;
  25. private final String[] path={
  26. "/favicon.ico",
  27. "/book/**",
  28. "/user/login.html",
  29. "/user/__MACOSX/**",
  30. "/user/css/**",
  31. "/user/fonts/**",
  32. "/user/images/**"};
  33. @Bean
  34. public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
  35. http.addFilterBefore(cookieToHeadersFilter, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
  36. //SecurityWebFiltersOrder枚举类定义了执行次序
  37. http.authorizeExchange(exchange -> exchange // 请求拦截处理
  38. .pathMatchers(path).permitAll()
  39. .pathMatchers(HttpMethod.OPTIONS).permitAll()
  40. .anyExchange().access(authorizationManager)//权限
  41. //.and().authorizeExchange().pathMatchers("/user/normal/**").hasRole("ROLE_USER")
  42. //.and().authorizeExchange().pathMatchers("/user/admin/**").hasRole("ROLE_ADMIN")
  43. //也可以这样写 将匹配路径和角色权限写在一起
  44. )
  45. .httpBasic()
  46. .and()
  47. .formLogin().loginPage("/user/login")//登录接口
  48. .authenticationSuccessHandler(authenticationSuccessHandler) //认证成功
  49. .authenticationFailureHandler(authenticationFaillHandler) //登陆验证失败
  50. .and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
  51. .accessDeniedHandler(accessDeniedHandler)//基于http的接口请求鉴权失败
  52. .and().csrf().disable()//必须支持跨域
  53. .logout().logoutUrl("/user/logout")
  54. .logoutHandler(logoutHandler)
  55. .logoutSuccessHandler(logoutSuccessHandler);
  56. http.securityContextRepository(securityRepository);
  57. //http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance());//无状态 默认情况下使用的WebSession
  58. return http.build();
  59. }
  60. @Bean
  61. public ReactiveAuthenticationManager reactiveAuthenticationManager() {
  62. LinkedList<ReactiveAuthenticationManager> managers = new LinkedList<>();
  63. managers.add(authentication -> {
  64. // 其他登陆方式
  65. return Mono.empty();
  66. });
  67. managers.add(new UserDetailsRepositoryReactiveAuthenticationManager(securityUserDetailsService));
  68. return new DelegatingReactiveAuthenticationManager(managers);
  69. }
  70. }

十一、测试

首先没有登录访问服务

然后登录 

访问服务

访问另一个接口

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发