赞
踩
目录
2.4.1 自定义登录成功回调ServerAuthenticationSuccessHandler
2.4.2 自定义登录失败回调ServerAuthenticationFailureHandler
2.4.3 自定义因未登录而访问未授权路径的回调ServerAuthenticationEntryPoint
2.4.4 自定义访问未授权资源路径的回调ServerAccessDeniedHandler
2.4.5 自定义成功退出登录的回调ServerLogoutSuccessHandler
spring gateway采用的是webflux的反应式实现,因此对应的sring security也需要用webflux的处理方式,个人整理的WebFlux应用的认证过程如下:
【注意】另外,如果要通过数据库或redis查找用户信息,可以重载实现ReactiveUserDetailsService的接口findByUsername,从其他数据源里查出user数据转成UserDetails对象,如果除了用户名和密码,还有其他额外的用户信息需要保存,可以重置UserDetails类,添加额外的信息
基本逻辑类都在包: spring-security-core-6.0.3.jar中
基本的重载方法,为了支持从数据库查找用户登录信息,实现接口类:UserDetails
- package com.cloudservice.gateway_service.security;
- import java.util.Collection;
- import java.util.Set;
- import org.springframework.security.core.GrantedAuthority;
- import org.springframework.security.core.userdetails.UserDetails;
- public class GatewayUserDetails implements UserDetails {
- private Long id; // 用户id
- private String password; // 密码
- private String username; // 用户名
- private boolean enabled; // 帐户是否可用
- private Set<GatewayUserGrantedAuthority> authorities; // 权限信息
- public Long getId() {
- return id;
- }
- public void setId(Long id) {
- this.id = id;
- }
- @Override
- public Collection<? extends GrantedAuthority> getAuthorities() {
- return authorities;
- }
- public void setAuthorities(Set<GatewayUserGrantedAuthority> authorities) {
- this.authorities = authorities;
- }
- @Override
- public String getPassword() {
- return password;
- }
- public void setPassword(String password) {
- this.password = password;
- }
- @Override
- public String getUsername() {
- return username;
- }
- public void setUsername(String username) {
- this.username = username;
- }
-
- @Override
- public boolean isAccountNonExpired() {
- // TODO 帐号是到到期
- return true;
- }
- @Override
- public boolean isAccountNonLocked() {
- // TODO 帐号是否锁定
- return true;
- }
- @Override
- public boolean isCredentialsNonExpired() {
- // TODO 密码是否到期
- return true;
- }
- @Override
- public boolean isEnabled() {
- return this.enabled;
- }
- public void setEnabled(boolean enabled) {
- this.enabled = enabled;
- }
- }
定义用户表的领域类:Users
- package com.cloudservice.gateway_service.security;
- import java.util.HashSet;
- import java.util.Set;
- import jakarta.persistence.Entity;
- import jakarta.persistence.GeneratedValue;
- import jakarta.persistence.GenerationType;
- import jakarta.persistence.Id;
- import jakarta.validation.constraints.NotNull;
- import jakarta.validation.constraints.Size;
- import lombok.AllArgsConstructor;
- import lombok.Data;
- import lombok.NoArgsConstructor;
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- @Entity
- public class Users {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Long id;
- @NotNull
- @Size(min=5, message="Username must be at least 5 characters long")
- private String username;
- @NotNull
- private String password;
- private boolean enabled;
- public GatewayUserDetails to_user_details() {
- GatewayUserDetails userDetails = new GatewayUserDetails();
- userDetails.setId(id);
- userDetails.setUsername(username);
- userDetails.setPassword(password);
- userDetails.setEnabled(enabled);
- // 此处为了测试路径权限,写死order路径权限,实际中应该是从角色与路径的授权表groups_authorities里读取加载
- Set<GatewayUserGrantedAuthority> authorities = new HashSet<GatewayUserGrantedAuthority>();
- authorities.add(new GatewayUserGrantedAuthority("/order/"));
- userDetails.setAuthorities(authorities);
- return userDetails;
- }
- public static Users from_user_details(GatewayUserDetails userDetails) {
- return new Users(userDetails.getId()
- , userDetails.getUsername()
- , userDetails.getPassword()
- , userDetails.isEnabled());
- }
- }
简单通过使用JPA 的Repository类来根据用户名查数据库返回基本的用户信息:
- package com.cloudservice.gateway_service.security;
- import org.springframework.data.repository.CrudRepository;
- public interface UsersRepository extends CrudRepository<Users, Long>{
- Iterable<Users> findByUsername(String username);
- }
网关的nacos配置里加上mysql的datasource配置:
spring:
datasource:
url: jdbc:mysql://192.168.10.111:32001/gateway?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true
username: mysql用户名
password: mysql密码
driver-class-name: com.mysql.cj.jdbc.Driver
自定义加载用户信息的类ReactiveUserDetailsService:
- package com.cloudservice.gateway_service.security;
- import java.util.ArrayList;
- import java.util.List;
- import org.apache.commons.codec.digest.DigestUtils;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
- import org.springframework.security.core.userdetails.UserDetails;
- import org.springframework.stereotype.Component;
- import lombok.extern.slf4j.Slf4j;
- import reactor.core.publisher.Flux;
- import reactor.core.publisher.Mono;
- @Slf4j
- @Component
- public class GatewayReactiveUserDetailsService implements ReactiveUserDetailsService {
- @Autowired
- private UsersRepository usersRepository;
- @Override
- public Mono<UserDetails> findByUsername(String username) {
- // TODO 优先查找缓存再查找数据库
- // String Val1 = passwordEncoder.encode("123456");
- log.info("gateway find user: {} {}", username, DigestUtils.sha256Hex("123456"));
- List<UserDetails> userDetailsList = new ArrayList<UserDetails>();
- usersRepository.findByUsername(username).forEach(user -> {
- userDetailsList.add(user.to_user_details());
- });
- return Flux.fromIterable(userDetailsList).next();
- }
- }
- package com.cloudservice.gateway_service.security;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
- import org.springframework.security.config.web.server.ServerHttpSecurity;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.security.crypto.password.PasswordEncoder;
- import org.springframework.security.web.server.SecurityWebFilterChain;
- @Configuration
- @EnableWebFluxSecurity
- public class GatewaySecurityConfig {
- @Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
- @Bean
- public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
- http
- .authorizeExchange()
- .pathMatchers("/favicon.*", "/login", "/logout").permitAll()
- .anyExchange().authenticated()
- .and().formLogin()
- .and().csrf().disable();
- return http.build();
- }
- }
重载路径权限验证类 ReactiveAuthorizationManager<AuthorizationContext>,代码如下:
- package com.cloudservice.gateway_service.security;
- import org.springframework.security.authentication.AuthenticationTrustResolver;
- import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
- import org.springframework.security.authorization.AuthorizationDecision;
- import org.springframework.security.authorization.ReactiveAuthorizationManager;
- import org.springframework.security.core.Authentication;
- import org.springframework.security.web.server.authorization.AuthorizationContext;
- import org.springframework.stereotype.Component;
- import reactor.core.publisher.Mono;
- @Component
- public class GatewayReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext>{
- private AuthenticationTrustResolver authTrustResolver = new AuthenticationTrustResolverImpl();
-
- // 验证通过则返回:AuthorizationDecision(true)
- // 验证失败则返回:AuthorizationDecision(false)
- @Override
- public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext context) {
- return authentication
- .filter(authentication_filter -> select_context(authentication_filter, context))
- .map(authentication_map -> authenticate(authentication_map, context))
- .defaultIfEmpty(new AuthorizationDecision(false));
- }
- private boolean select_context(Authentication authentication, AuthorizationContext context) {
- // String req_path = context.getExchange().getRequest().getURI().getPath();
- // log.info("check filter path: {}", req_path);
- return !this.authTrustResolver.isAnonymous(authentication);
- }
- private AuthorizationDecision authenticate(Authentication authentication, AuthorizationContext context) {
- if (authentication.isAuthenticated()) {
- // 判断context.getExchange().getRequest().getPath()是否在authentication_notanonymous.getAuthorities()集合中
- String req_path = context.getExchange().getRequest().getURI().getPath();
- if (authentication.getAuthorities().contains(new GatewayUserGrantedAuthority(req_path)) == false) {
- return new AuthorizationDecision(false);
- }
- }
- return new AuthorizationDecision(authentication.isAuthenticated());
- }
- }
用户可访问路径的集合存放在authorities集合里,为了支持集合类的contains操作,必须重载GrantedAuthority类并实现equals和hashCode接口,具体实现代码如下:
- package com.cloudservice.gateway_service.security;
-
- import org.springframework.security.core.GrantedAuthority;
-
- public class GatewayUserGrantedAuthority implements GrantedAuthority, java.lang.Comparable<Object> {
- String authority;
- public GatewayUserGrantedAuthority() {}
- public GatewayUserGrantedAuthority(String authority) {
- this.authority = authority;
- }
- @Override
- public String getAuthority() {
- return this.authority;
- }
- public void setAuthority(String authority) {
- this.authority = authority;
- }
- @Override
- public int compareTo(Object obj) {
- if (obj instanceof GatewayUserGrantedAuthority) {
- return this.authority.compareTo(((GatewayUserGrantedAuthority)obj).getAuthority());
- }
- return 1;
- }
- public boolean equals(Object obj) {
- if (!(obj instanceof GatewayUserGrantedAuthority)) {
- return false;
- }
- if (obj == this) {
- return true;
- }
- return this.authority.equals(((GatewayUserGrantedAuthority)obj).getAuthority());
- }
- public int hashCode() {
- return this.authority.hashCode();
- }
- }
修改配置类GatewaySecurityConfig的springSecurityFilterChain接口,引用路径鉴权类GatewayReactiveAuthorizationManager:
- @Configuration
- @EnableWebFluxSecurity
- public class GatewaySecurityConfig {
- @Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
- @Autowired
- private GatewayReactiveAuthorizationManager gatewayReactiveAuthorizationManager;
- @Bean
- public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
- http
- .authorizeExchange()
- .pathMatchers("/favicon.ico", "/login", "/logout").permitAll()
- .anyExchange().access(gatewayReactiveAuthorizationManager)
- .and().formLogin()
- .and().csrf().disable();
- return http.build();
- }
- }
通过自定义登录成功时的回调函数,可以自定义登录成功后的返回消息,vue3中可以直接返回json格式,方便判断后在vue3的js代码中实现提示后跳转逻辑,实现代码如下:
- package com.cloudservice.gateway_service.security;
- import org.springframework.core.io.buffer.DataBuffer;
- import org.springframework.http.server.reactive.ServerHttpResponse;
- import org.springframework.security.core.Authentication;
- import org.springframework.security.web.server.WebFilterExchange;
- import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
- import org.springframework.stereotype.Component;
- import org.springframework.web.server.ServerWebExchange;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import lombok.extern.slf4j.Slf4j;
- import reactor.core.publisher.Mono;
- @Slf4j
- @Component
- public class CustomLoginSuccessHandler implements ServerAuthenticationSuccessHandler{
- @Override
- public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
- ServerWebExchange exchange = webFilterExchange.getExchange();
- ServerHttpResponse response = exchange.getResponse();
- response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
-
- log.info("user login success: {}", authentication.getName());
-
- Object principal = authentication.getPrincipal();
- ObjectMapper objectMapper = new ObjectMapper();
- DataBuffer bodyDataBuffer = null;
- try {
- bodyDataBuffer = response.bufferFactory().wrap(objectMapper.writeValueAsBytes(principal));
- } catch (Exception e) {
- e.printStackTrace();
- }
- return response.writeWith(Mono.just(bodyDataBuffer));
- }
- }
通过自定义登录失败回调,可以定制返回登录失败的消息,vue3里一般需要返回json格式数据,实现代码如下:
- package com.cloudservice.gateway_service.security;
- import java.util.HashMap;
- import org.springframework.core.io.buffer.DataBuffer;
- import org.springframework.http.HttpStatus;
- import org.springframework.http.server.reactive.ServerHttpResponse;
- import org.springframework.security.authentication.BadCredentialsException;
- import org.springframework.security.authentication.LockedException;
- import org.springframework.security.core.AuthenticationException;
- import org.springframework.security.web.server.WebFilterExchange;
- import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
- import org.springframework.stereotype.Component;
- import com.alibaba.fastjson.JSON;
- import lombok.extern.slf4j.Slf4j;
- import reactor.core.publisher.Mono;
- @Slf4j
- @Component
- public class CustomLoginFailureHandler implements ServerAuthenticationFailureHandler {
- @Override
- public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
- ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
- response.setStatusCode(HttpStatus.FORBIDDEN);
- response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
-
- log.info("user login fail: {}", webFilterExchange.getExchange().getRequest().getPath());
-
- HashMap<String, String> map = new HashMap<String, String>();
- map.put("code", "-1001");
- if (exception instanceof LockedException) {
- map.put("message", "账户被锁定,请联系管理员!");
- } else if (exception instanceof BadCredentialsException) {
- map.put("message", "用户名或者密码输入错误,请重新输入!");
- } else {
- map.put("message", exception.getMessage());
- }
- DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(map));
- return response.writeWith(Mono.just(dataBuffer));
- }
- }
通过自定义未登录而访问路径的错误回调 ,可以返回vue3需要的json格式,也可以强制跳转到登录页面,此处实现是强制跳转页面:
- package com.cloudservice.gateway_service.security;
- import java.net.URI;
- import java.util.HashMap;
- import org.springframework.core.io.buffer.DataBuffer;
- import org.springframework.http.HttpStatus;
- import org.springframework.http.server.reactive.ServerHttpResponse;
- import org.springframework.security.core.AuthenticationException;
- import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
- import org.springframework.stereotype.Component;
- import org.springframework.web.server.ServerWebExchange;
- import com.alibaba.fastjson.JSON;
- import lombok.extern.slf4j.Slf4j;
- import reactor.core.publisher.Mono;
- @Slf4j
- @Component
- public class CustomNoLoginHandler implements ServerAuthenticationEntryPoint {
- @Override
- public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
- return Mono.fromRunnable(() -> {
- ServerHttpResponse response = exchange.getResponse();
- response.setStatusCode(HttpStatus.FOUND);
- response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
- // 强制跳转到登录页面,在vue3中一般是返回json数据,并交由vue3来跳转
- response.getHeaders().setLocation(URI.create("/login"));
-
- log.info("url when no login: {}", exchange.getRequest().getPath());
-
- HashMap<String, Object> map = new HashMap<>();
- map.put("code", HttpStatus.FOUND.value());
- map.put("message", "暂未登录,请您先进行登录");
- DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(map));
- response.writeWith(Mono.just(dataBuffer));
- });
- }
- }
【注意】加了此自定义回调时,不能正常访问security默认定义的login页面,需要自定义登录页面,本人简单的实现示例:
此示例的controller使用到thymeleaf的模板,因此需要在pom.xml中引用依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
创建一个用来接收GET方式的login请求的controller:
- package com.cloudservice.gateway_service.security;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- @Controller
- public class CustomLoginControl {
- @GetMapping("/login")
- public String login() {
- return "login";
- }
- }
在src/main/resources/templates/目录下定义登录页面模板login.html
- <html lang="en"><head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
- <title>Please sign in</title>
- </head>
- <body>
- <div class="container">
- <form method="post" action="/login">
- <h2>Please sign in</h2>
- <p>
- <label>Username</label>
- <input type="text" id="username" name="username" placeholder="Username" required="" autofocus="">
- </p>
- <p>
- <label>Password</label>
- <input type="password" id="password" name="password" placeholder="Password" required="">
- </p>
- <button type="submit">Sign in</button>
- </form>
- </div>
- </body>
- </html>
当访问资源路径进行权限验证时,ReactiveAuthorizationManager验证不通过,则会回调此类的接口,可以自定义返回的数据格式,代码如下:
- package com.cloudservice.gateway_service.security;
- import java.util.HashMap;
- import org.springframework.core.io.buffer.DataBuffer;
- import org.springframework.http.HttpStatus;
- import org.springframework.http.server.reactive.ServerHttpResponse;
- import org.springframework.security.access.AccessDeniedException;
- import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
- import org.springframework.stereotype.Component;
- import org.springframework.web.server.ServerWebExchange;
- import com.alibaba.fastjson.JSON;
- import lombok.extern.slf4j.Slf4j;
- import reactor.core.publisher.Mono;
- @Slf4j
- @Component
- public class CustomUrlNoRightHandler implements ServerAccessDeniedHandler{
- @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");
-
- log.info("url no right: {}", exchange.getRequest().getPath());
-
- HashMap<String, String> map = new HashMap<>();
- map.put("code", "-1002");
- map.put("message", "资源路径无访问权限!");
- DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(map));
- return response.writeWith(Mono.just(dataBuffer));
- }
- }
用于自定义退出登录是的返回数据格式,示例代码如下:
- package com.cloudservice.gateway_service.security;
- import java.util.HashMap;
- import org.springframework.core.io.buffer.DataBuffer;
- import org.springframework.http.HttpStatus;
- import org.springframework.http.server.reactive.ServerHttpResponse;
- import org.springframework.security.core.Authentication;
- import org.springframework.security.web.server.WebFilterExchange;
- import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
- import org.springframework.stereotype.Component;
- import com.alibaba.fastjson.JSON;
- import lombok.extern.slf4j.Slf4j;
- import reactor.core.publisher.Mono;
- @Slf4j
- @Component
- public class CustomLogoutSuccessHandler implements ServerLogoutSuccessHandler {@Override
- public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
- ServerHttpResponse response = exchange.getExchange().getResponse();
- response.setStatusCode(HttpStatus.OK);
- response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
-
- log.info("user logout success: {}", authentication.getName());
-
- HashMap<String, String> map = new HashMap<>();
- map.put("code", "0");
- map.put("message", "退出登录成功!");
- DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(map));
- return response.writeWith(Mono.just(dataBuffer));
- }
- }
- package com.cloudservice.gateway_service.security;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
- import org.springframework.security.config.web.server.ServerHttpSecurity;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.security.crypto.password.PasswordEncoder;
- import org.springframework.security.web.server.SecurityWebFilterChain;
- @Configuration
- @EnableWebFluxSecurity
- public class GatewaySecurityConfig {
- @Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
- @Autowired
- private GatewayReactiveAuthorizationManager gatewayReactiveAuthorizationManager;
- @Autowired
- private CustomLoginFailureHandler customLoginFailureHandler;
- @Autowired
- private CustomLoginSuccessHandler customLoginSuccessHandler;
- @Autowired
- private CustomNoLoginHandler customNoLoginHandler;
- @Autowired
- private CustomLogoutSuccessHandler customLogoutSuccessHandler;
- @Autowired
- private CustomUrlNoRightHandler customUrlNoRightHandler;
- @Bean
- public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
- http
- .authorizeExchange()
- .pathMatchers("/favicon.ico", "/login", "/logout").permitAll()
- .anyExchange().access(gatewayReactiveAuthorizationManager)
- .and().formLogin()
- .authenticationFailureHandler(customLoginFailureHandler)
- .authenticationSuccessHandler(customLoginSuccessHandler)
- .and().exceptionHandling()
- .accessDeniedHandler(customUrlNoRightHandler)
- .authenticationEntryPoint(customNoLoginHandler)
- .and().logout().logoutSuccessHandler(customLogoutSuccessHandler)
- .and().csrf().disable();
- return http.build();
- }
- }
【注意】
1. 登录会话的超时配置,可以修改nacos配置,加上:
server:
reactive:
session:
timeout: 1m # session超时时间为1分钟, 默认是60分钟
2. 为了防止集中登录导致的登录数据库过载,修改GatewayReactiveUserDetailsService类的findByUsername接口实现,引入redis,优先查看redis缓存里有没有用户信息,有则从redis中加载,否则才从mysql数据库中加载
[上一篇]从零开始搭建高负载java架构(04)——gateway网关节点(动态路由)
[下一篇]从零开始搭建高负载java架构(06):gateway网关节点(sentinel篇)
参考资料:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。