赞
踩
原本打算将Security模块与gateway模块分开写的,但想到gateway本来就有过滤的作用 ,于是就把gateway和Security结合在一起了,然后结合JWT令牌对用户身份和权限进行校验。
Spring Cloud的网关与传统的SpringMVC不同,gateway是基于Netty容器,采用的webflux技术,所以gateway模块不能引入spring web包。虽然是不同,但是在SpringMVC模式下的Security实现步骤和流程都差不多。
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>
- 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();//获取日历对象
- if(expireTime <=0)
- instance.add(Calendar.SECOND,3600);//默认一小时
- 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;
-
- }
- }
- 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) {
- //调用数据库根据用户名获取用户
- 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);
- }
-
- }
- }
这里我为了方便测试,只设置了两个用户,admin和晢user,用户角色也只有一种。
二、AuthenticationSuccessHandler,定义认证成功类
- @Component
- @Slf4j
- public class AuthenticationSuccessHandler extends WebFilterChainServerAuthenticationSuccessHandler {
- @Value("${login.timeout}")
- private int timeout=3600;//默认一小时
- 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();
- //设置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");
- //设置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());//这里只添加了一种角色 实际上用户可以有不同的角色类型
- 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字段,然后后端判断有没有这个字段,没有的话就不记住。
- @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));
- }
- }
- @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();
- }
- }
当客户端访问服务接口的时候,如果是有效token,那么就根据token来判断用户权限,实现ServerSecurityContextRepository 类的主要目的是实现load方法,这个方法实际上是传递一个Authentication对象供后面ReactiveAuthorizationManager<AuthorizationContext>来判断用户权限。我这里只传递了用户的role信息,所以就没有去实现ReactiveAuthorizationManager这个接口了。
Security框架默认提供了两个ServerSecurityContextRepository实现类,WebSessionServerSecurityContextRepository和NoOpServerSecurityContextRepository,Security默认使用WebSessionServerSecurityContextRepository,这个是使用session来保存用户登录状态的,NoOpServerSecurityContextRepository是无状态的。
如果客户端没有认证授权就直接访问服务接口,然后就会调用这个类,返回的状态码是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));
- }
- }
-
当访问服务接口的用户权限不够时会调用这个类,返回HTTP状态码是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", "未授权禁止访问");
- 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));
- }
- }
- @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);
- //对客户端访问路径与用户角色进行匹配
- }
- return new AuthorizationDecision(false);
- }).defaultIfEmpty(new AuthorizationDecision(false));
- }
- }
返回new AuthorizationDecision(true)代表授予权限访问服务,为false则是拒绝。
- @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();
- //设置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");
- //设置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));
- }
- }
- @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);
-
- }
-
- }
这里需要注意的是,如果要想在认证前后过滤Http请求,用全局过滤器或者局部过滤器是不起作用的,因为它们总是在鉴权通过后执行,也就是它们的执行顺序始终再Security过滤器之后,无论order值多大多小。这时候必须实现的接口是WebFilter而不是GlobalFilter或者GatewayFilter,然后将接口实现类添加到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枚举类定义了执行次序
- 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());//无状态 默认情况下使用的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);
- }
-
- }
-
-
-
-
首先没有登录访问服务
然后登录
访问服务
访问另一个接口
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。