背景
- jwt token的载荷是明文(base64),虽然只是用来传递一些非敏感信息,但依旧会让人感觉有些不适
- jwt token无法主动失效
- 微服务之间尽量减少耦合度
解决思路
- 由认证服务(iam)产生RedisToken,该token保存在iam服务的redis中,可以主动失效,也可以设置失效时间
- 采用jwt作为微服务间验证的依据,如果使用RedisToken,则所有微服务均需要依赖同一个redis数据库(或集群)
实现方式
- 用户登陆,由 认证服务 产生RedisToken交给用户
- 用户使用RedisToken通过 网关服务 访问 业务服务,在 网关服务 中使用RedisToken交换JwtToken(可以包含一些非敏感的当前用户的信息),再使用JwtToken访问业务服务
- 业务服务只对JwtToken进行验证,并且可以从jwt payload中解析出当前用户的信息
代码实现
注册中心
eureka、nacos等,略
认证服务
使用SpringSecurity实现用户登陆
- 依赖注入
其他如数据库,redis,服务发现,服务调用等依赖就不写了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 设置用户密码加密方式
/**
* 密码加密方式
*
* @author Jenson
*/
@Component
@Slf4j
public class CustomBCryptPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
// 简单加密,生成一个salt
String salt = BCrypt.gensalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword != null && encodedPassword != null && !encodedPassword.isEmpty()) {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
} else {
log.warn("Empty encoded password");
return false;
}
}
}
- 登录认证过滤器,设置登录地址、调用方式,继承
org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter
/**
* 登录认证过滤器
*
* @author Jenson
*/
public class AuthenticationLoginFilter extends AbstractAuthenticationProcessingFilter {
/**
* 设置登录地址、调用方式
*/
public AuthenticationLoginFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 读取表单提取数据
String username = request.getParameter("username");
String password = request.getParameter("password");
// 封装到token中提交
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(authRequest);
}
}
- 实现
org.springframework.security.core.userdetails.UserDetailsService
验证登陆接口中用户传入的账号密码,生成org.springframework.security.core.userdetails.UserDetails
/**
* @author Jenson
*/
@Slf4j
@Service
public class CustomUserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 使用用户名查询数据库(或是缓存中的)中的用户持久层对象 UserPO(名字随便了)
UserPO userPo = this.searchUserPoFromDb(username);
if(userPo == null){
// 用户不存在,登录失败
return null;
}
// 构建 org.springframework.security.core.userdetails.User 对象,当然最好是继承它,可以添加一些自己的属性上去,比如生日年龄性别啥的
User user =new User(userPo.getUsername,
userPo.getPassword,
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
return user ;
}
}
- 实现 认证成功处理器,
org.springframework.security.web.authentication.AuthenticationSuccessHandler
认证成功后,生成token,输出token
/**
* 认证成功处理器
*
* @author Jenson
*/
@Slf4j
@Component
public class LoginAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
// 这是自己写的redis-token工具对象
@Autowired
private RedisTokenUtils redisTokenUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
User user = (User) authentication.getPrincipal();
SecurityContextHolder.getContext().setAuthentication(authentication);
// 根据获取的用户信息生成token,并将token保存在redis中,设置失效时间,
// 考虑到登陆时,系统中可能有未失效的token,为了避免多端登陆互相踢出,可以先尝试获取用户的token,
// 如果存在则刷新缓存时间(token续期),更新token绑定用户缓存信息,返回老token;如果不存在则沈城新token
UserTokenRel userTokenRel = redisTokenUtils.refreshToken(user);
// 书写响应体
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.OK.value());
// 在响应体中写出token
byte[] body = JSON.toJSONString(userTokenRel.getLoginToken()).getBytes(StandardCharsets.UTF_8);
OutputStream outputStream = response.getOutputStream();
try {
outputStream.write(body);
} finally {
outputStream.flush();
outputStream.close();
}
}
}
- 实现 认证失败处理器,
org.springframework.security.web.authentication.AuthenticationFailureHandler
/**
* 认证失败处理
*
* @author Jenson
*/
@Slf4j
@Component
public class LoginAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
String exceptionMsg = "认证失败原因";
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
OutputStream outputStream = response.getOutputStream();
// 自定义的响应体
CuxResponseEntity<ExceptionResponse> responseEntity =
new CuxResponseEntity<>(new ExceptionResponse("AUTHENTICATION_FAILURE", exceptionMsg), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
byte[] body = JSON.toJSONString(responseEntity.getBody()).getBytes(StandardCharsets.UTF_8);
try {
outputStream.write(body);
} finally {
outputStream.flush();
outputStream.close();
}
}
}
- 登录过滤器的配置
需要使用到上述创建的三个实例
/**
* 登录过滤器的配置
*
* @author Jenson
*/
@Configuration
public class AuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
/**
* userDetailService
*/
@Qualifier("customUserDetailsServiceImpl")
@Autowired
private UserDetailsService userDetailsService;
/**
* 登录成功处理器
*/
@Autowired
private AuthenticationSuccessHandler loginAuthenticationSuccessHandler;
/**
* 登录失败处理器
*/
@Autowired
private AuthenticationFailureHandler loginAuthenticationFailureHandler;
/**
* 加密
*/
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 将登录接口的过滤器配置到过滤器链中
* 1. 配置登录成功、失败处理器
* 2. 配置自定义的userDetailService(从数据库中获取用户数据)
* 3. 将自定义的过滤器配置到spring security的过滤器链中,配置在UsernamePasswordAuthenticationFilter之前
*
* @param http HttpSecurity
*/
@Override
public void configure(HttpSecurity http) {
AuthenticationLoginFilter filter = new AuthenticationLoginFilter();
filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//认证成功处理器
filter.setAuthenticationSuccessHandler(loginAuthenticationSuccessHandler);
//认证失败处理器
filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);
//直接使用DaoAuthenticationProvider
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//设置userDetailService
provider.setUserDetailsService(userDetailsService);
//设置加密算法
provider.setPasswordEncoder(passwordEncoder);
http.authenticationProvider(provider);
//将这个过滤器添加到UsernamePasswordAuthenticationFilter之前执行
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
}
- Token校验过滤器
此处的token,是jwt-token,在上述“认证成功过滤器”中,发放的是redis-token,但是使用redis-token调用网关接口时,网关会使用redis-token交换jwt-token,所以该认证服务的filter需要认证的是jwt-token
/**
* Token校验过滤器
*
* @author Jenson
*/
@Slf4j
public class TokenAuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
HttpServletRequest httpServletRequest;
httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("Authorization");
log.info("---------> Authorization : {}", authorization);
if (StringUtils.hasText(authorization)) {
String[] tokenDetail = authorization.trim().split(BaseConstants.Symbol.SPACE);
String token = tokenDetail[1];
if (StringUtils.hasText(token)) {
User user = null;
try {
// JwtUtils 是自定义的jwt-token解析工具
// 解析jwt-token,创建用户对象
// HttpServletResponseUtils 是自定义的异常打印工具类
User = new User(JwtUtils.verify(token));
} catch (UnauthorizedException e) {
HttpServletResponseUtils.outPrintUnauthorizedException(httpServletResponse);
return;
} catch (ForbiddenException e) {
HttpServletResponseUtils.outPrintForbiddenException(httpServletResponse);
return;
} catch (Exception e) {
HttpServletResponseUtils.outPrintUnknownException(httpServletResponse);
return;
}
// 账号不为空且还没有认证过
if (user !=null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 认证成功,设置当前用户对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
该过滤器没有像参考文档一样继承OncePerRequestFilter
,以为我实际使用中遇到了非自定义异常时,又会过一遍filter才抛出(不知道是不是哪配置错了),而这次过filter时filter调用链中没有了认证的filter,就会抛出认证失败的异常,将原异常覆盖。
- 接口未认证异常
/**
* 用户未通过认证访问受保护的资源 401
*
* @author Jenson
*/
@Slf4j
@Component
public class EntryPointUauthenticationHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
authException.printStackTrace();
// 自定义异常打印工具
HttpServletResponseUtils.outPrintUnauthorizedException(response);
}
}
- 接口认证无权限异常
/**
* 认证成功的用户访问受保护的资源,但是权限不够 403
*
* @author Jenson
*/
@Slf4j
@Component
public class RequestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
// 自定义异常打印工具
HttpServletResponseUtils.outPrintForbiddenException(response);
}
}
- 接口授权配置
/**
* @author Jenson
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private AuthenticationSecurityConfig authenticationSecurityConfig;
@Qualifier("entryPointUauthenticationHandler")
@Autowired
private AuthenticationEntryPoint entryPointUauthenticationHandler;
@Qualifier("requestAccessDeniedHandler")
@Autowired
private AccessDeniedHandler requestAccessDeniedHandler;
/**
* 授权配置,最高优先级
*
* @param http HttpSecurity
* @return SecurityFilterChain
* @throws Exception
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用表单登录
.formLogin().disable()
// 应用登录过滤器的配置,配置分离
.apply(authenticationSecurityConfig)
.and()
// 设置URL的授权
.authorizeHttpRequests()
// 这里需要将登录页面放行,permitAll()表示不再拦截,/login 登录的url,/refreshToken刷新token的url
.requestMatchers(
// 登陆接口
"/login",
// token交换接口(redis-token -> jwt-token)
"/token/generate/jwt/{redisToken}"
)
.permitAll()
.anyRequest()
.authenticated()
//处理异常情况:认证失败和权限不足
.and()
.exceptionHandling()
//认证未通过,不允许访问异常处理器
.authenticationEntryPoint(entryPointUauthenticationHandler)
//认证通过,但是没权限处理器
.accessDeniedHandler(requestAccessDeniedHandler)
.and()
//禁用session,JWT校验不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//将TOKEN校验过滤器配置到过滤器链中,否则不生效,放到UsernamePasswordAuthenticationFilter之前
.addFilterAt(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 解决跨域问题(其实没有解决)
.cors()
.and()
// 关闭csrf
.csrf().disable();
return http.build();
}
}
将登陆接口(/login
)和 token交换接口(/token/generate/jwt/{redisToken}
) 设置为免登录。
/token/generate/jwt/{redisToken}
其实是需要认证的,但是filter配置为了认证jwt-token,当然也可以在filter中增加一种认证方式,但我这里选择在容器内认证,所以该接口设置为免登录。
- token 交换接口
/token/generate/jwt/{redisToken}
/**
* 生成JWT
*
* @param redisToken redisToken
* @return jwt
*/
@GetMapping("/generate/jwt/{redisToken}")
public WmResponseEntity<String> generateJwt(@PathVariable String redisToken) {
log.info("-----> generateJwt redisToken : {}", redisToken);
// 根据 redisToken 获取用户信息 ,redisTokenUtils 为自定义用户redis-token管理工具
UserTokenRel userTokenRel = redisTokenUtils.getUserTokenRel(redisToken);
if (userTokenRel == null) {
// CommonException 为自定义 RuntimeException
throw new CommonException("NOT_LOGGED_IN", "用户未登录");
}
User user = userService.searchUserByUserId(userTokenRel.getUserId());
// 生成 jwt-token,jwt的payload中可以尽可能装入除用户密码外的用户信息,
// 在业务服务中进行解析,就不需要跨服务获取调用者信息了,实现了服务解耦
String jwt = tokenService.generateJwt(user);
// 异步刷新一下token,避免使用中失效
Long userId = user.getUserId();
// 异步刷新token,token自动续期,防止用户用着用着突然掉线
CompletableFuture.runAsync(() -> redisTokenUtils.refreshToken(userId), commonExecutor);
return Results.success(jwt);
}
网关
- 依赖注入
其他工具依赖就省略了
<!-- gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>4.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- gateway 路由配置
无论采用静态路由配置还是动态路由配置,不在此处细说
配置认证服务的路由前缀为/iam
,则登陆接口地址就是/iam/login
spring-gateway+nacos 实现动态路由配置可以参考以下文章:
https://blog.csdn.net/qq_38374397/article/details/125874882
- 实现
GlobalFilter
,拦截请求进行客户化处理
自定义filter,在网关路由前调用认证服务接口交换token(只能异步调用),使用jwt调用服务
/**
* 网关请求拦截客户化处理
*
* @author Jenson
* @version 1.0
*/
@Slf4j
@Component
public class CuxGlobalFilter implements GlobalFilter, Ordered {
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String path = request.getURI().getPath();
log.info("-----------> path : {}", path);
HttpHeaders headers = request.getHeaders();
String authorization = headers.getFirst("Authorization");
String tenant = headers.getFirst("Tenant");
if (!"/iam/login".equals(path) && StringUtils.hasText(authorization)) {
// 非 认证 服务,换用 jwt token,此举是为了在认证层面解耦服务
headers = HttpHeaders.writableHttpHeaders(headers);
String[] tokenDetail = authorization.trim().split(BaseConstants.Symbol.SPACE);
String token = tokenDetail[1];
if (!StringUtils.hasText(token)) {
return outUnauthorizedResponse(response);
}
// ------ 获取Token start -----
// 在此处,异步 feign调用 交换 token 接口,获得新jwt-token
CompletableFuture<ResponseEntity<String>> newJwtFuture = CompletableFuture.supplyAsync(() -> tokenService.generateJwt(token));
String jwt;
try {
ResponseEntity<String> responseEntity = newJwtFuture.get(1, TimeUnit.SECONDS);
// FeignRspEntityParseUtils 自定义接口响应结果解析工具
jwt = FeignRspEntityParseUtils.parse(responseEntity, String.class);
} catch (CommonException e) {
if ("NOT_LOGGED_IN".equals(e.getCode())) {
return outUnauthorizedResponse(response);
}
return outCommonExceptionResponse(response, e.getCode(), e.getMsg());
} catch (Exception e) {
log.info("----> 换取 jwt 失败 , {}", e);
return outCommonExceptionResponse(response, "SWITCH_JWT_ERROR", "换取 jwt 失败 ,请联系管理员检查认证服务");
}
log.info("-----> jwt : {}", jwt);
// ---- 在这里获取Token end -----
headers.setBearerAuth(jwt);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
/**
* 输出未认证响应
*
* @param response 响应体
* @return 未认证
* @throws JsonProcessingException json解析异常
*/
private Mono<Void> outUnauthorizedResponse(ServerHttpResponse response) throws JsonProcessingException {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
DataBufferFactory bufferFactory = response.bufferFactory();
ObjectMapper objectMapper = new ObjectMapper();
CuxResponseEntity<ExceptionResponse> responseEntity =
new CuxResponseEntity<>(new ExceptionResponse("UNAUTHORIZED", "Unauthorized !"), new HttpHeaders(), HttpStatus.UNAUTHORIZED);
DataBuffer wrap = bufferFactory.wrap(objectMapper.writeValueAsBytes(responseEntity.getBody()));
return response.writeWith(Mono.fromSupplier(() -> wrap));
}
/**
* 输出通用异常响应信息
*
* @param response 响应体
* @return 未认证
* @throws JsonProcessingException json解析异常
*/
private Mono<Void> outCommonExceptionResponse(ServerHttpResponse response, String code, String msg) throws JsonProcessingException {
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
DataBufferFactory bufferFactory = response.bufferFactory();
ObjectMapper objectMapper = new ObjectMapper();
CuxResponseEntity<ExceptionResponse> responseEntity =
new CuxResponseEntity<>(new ExceptionResponse(code, msg), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
DataBuffer wrap = bufferFactory.wrap(objectMapper.writeValueAsBytes(responseEntity.getBody()));
return response.writeWith(Mono.fromSupplier(() -> wrap));
}
}
- 自定义异常处理(非必须)
将网关抛的错改造为自定义的异常格式,方便前端处理
/**
* 自定义异常处理
*
* @author Jenson
* @version 1.0
*/
@Slf4j
@Configuration
@Order(-1)
public class CuxWebExceptionHandler implements WebExceptionHandler {
@SneakyThrows
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
} else if (ex instanceof ResponseStatusException rspEx) {
HttpStatusCode httpStatusCode = rspEx.getStatusCode();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.setStatusCode(httpStatusCode);
DataBufferFactory bufferFactory = response.bufferFactory();
ObjectMapper objectMapper = new ObjectMapper();
CuxResponseEntity<ExceptionResponse> responseEntity =
new CuxResponseEntity<>(new ExceptionResponse(((HttpStatus) httpStatusCode).name(), rspEx.getReason()),
rspEx.getHeaders(), httpStatusCode.value());
DataBuffer wrap = bufferFactory.wrap(objectMapper.writeValueAsBytes(responseEntity.getBody()));
return response.writeWith(Mono.fromSupplier(() -> wrap));
} else {
return Mono.error(ex);
}
}
}
- 解决跨域
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* 跨域
*
* @author Jenson
* @version 1.0
*/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许的请求头
config.addAllowedMethod("*");
// 允许的请求源 (如:http://localhost:8080)
config.addAllowedOrigin("*");
// 允许的请求方法 ==> GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
config.addAllowedHeader("*");
// URL 映射 (如: /admin/**)
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
业务服务
业务服务的认证逻辑都是统一的,所以采用依赖starter组件的方式,就可以快速为每一个业务服务增加接口认证
- 构建
cux-start-core
工程
- pom.xml 依赖注入
......
<groupId>com.cux</groupId>
<artifactId>cux-starter-core</artifactId>
<version>1.0-SNAPSHOT</version>
<name>cux-starter-core</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
......
- 接口认证filter(jwt)
此处代码逻辑和认证服务是一致的,其实可以考虑将此核心依赖包运用于认证服务
/**
* Token校验过滤器
*
* @author Jenson
*/
@Slf4j
public class TokenAuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
HttpServletRequest httpServletRequest;
httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("Authorization");
log.info("---------> Authorization : {}", authorization);
if (StringUtils.hasText(authorization)) {
String[] tokenDetail = authorization.trim().split(BaseConstants.Symbol.SPACE);
String token = tokenDetail[1];
if (StringUtils.hasText(token)) {
User user = null;
try {
// JwtUtils 是自定义的jwt-token解析工具
// 解析jwt-token,创建用户对象
// HttpServletResponseUtils 是自定义的异常打印工具类
User = new User(JwtUtils.verify(token));
} catch (UnauthorizedException e) {
HttpServletResponseUtils.outPrintUnauthorizedException(httpServletResponse);
return;
} catch (ForbiddenException e) {
HttpServletResponseUtils.outPrintForbiddenException(httpServletResponse);
return;
} catch (Exception e) {
HttpServletResponseUtils.outPrintUnknownException(httpServletResponse);
return;
}
// 账号不为空且还没有认证过
if (user !=null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 认证成功,设置当前用户对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
- 接口未认证异常
此处代码逻辑和认证服务是一致的,其实可以考虑将此核心依赖包运用于认证服务
/**
* 用户未通过认证访问受保护的资源 401
*
* @author Jenson
*/
@Slf4j
@Component
public class EntryPointUauthenticationHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
authException.printStackTrace();
// 自定义异常打印工具
HttpServletResponseUtils.outPrintUnauthorizedException(response);
}
}
- 接口认证无权限异常
此处代码逻辑和认证服务是一致的,其实可以考虑将此核心依赖包运用于认证服务
/**
* 认证成功的用户访问受保护的资源,但是权限不够 403
*
* @author Jenson
*/
@Slf4j
@Component
public class RequestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
// 自定义异常打印工具
HttpServletResponseUtils.outPrintForbiddenException(response);
}
}
- 认证配置
/**
* @author Jenson
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private PermitRequestMatchers permitRequestMatchers;
/**
* 授权配置,最高优先级
*
* @param http HttpSecurity
* @return SecurityFilterChain
* @throws Exception
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用表单登录
.formLogin().disable()
// 设置URL的授权
.authorizeHttpRequests()
// 需要放行的url,动态获取免登录接口
.requestMatchers(permitRequestMatchers.requestMatchersToArray())
.permitAll()
.anyRequest()
.authenticated()
//处理异常情况:认证失败和权限不足
.and()
.exceptionHandling()
//认证未通过,不允许访问异常处理器
.authenticationEntryPoint(new EntryPointUauthenticationHandler())
//认证通过,但是没权限处理器
.accessDeniedHandler(new RequestAccessDeniedHandler())
.and()
//禁用session,JWT校验不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//将TOKEN校验过滤器配置到过滤器链中,否则不生效,放到UsernamePasswordAuthenticationFilter之前
.addFilterAt(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 解决跨域问题
.cors()
.and()
// 关闭csrf
.csrf().disable();
return http.build();
}
}
- 服务间调用,token传递(
openfeign
)
微服务间feign调用是不走网关的,为了互相传递当前调用接口的jwt-token,需要配置feign,token的传递可以进行公共配置
- feign 依赖
<!--服务调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
<version>4.0.1</version>
</dependency>
- Feign 请求拦截器
/**
* Feign 请求拦截器
* <p>
* 请求带上token
*
* @author Jenson
*/
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
String authorization = request.getHeader("Authorization");
requestTemplate.header("Authorization", authorization);
}
}
}
/**
* @author Jenson
*/
@Configuration
public class FeignRequestInterceptorConfig {
@Bean
public RequestInterceptor createFeignRequestInterceptor() {
return new FeignRequestInterceptor();
}
}
- spring.factories 配置文件
定义需要装载的配置类
- 打包,安装依赖
本地安装:mvn install
- 将该核心依赖运用于业务服务,即可完成对业务服务的接口权限控制
业务服务与认证服务、网关需要在同一个注册中心下;
网关需要配置对应业务服务的路由;
此时,直接访问业务服务的接口需要jwt-token,通过网关访问,需要redis-token;
只需将网关暴露到外部而不需要暴露业务服务
其他补充
- jwt 生成 和 解析
- pom.xml 依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.5.0</version>
</dependency>
- 生成jwt
/**
* 生成jwt,设置超时时间
*
* @param payload 载荷
* @return jwt
*/
public static String generate(Map<String, String> payload) {
//过期时间
Date expireDate = new Date(System.currentTimeMillis() + DEFAULT_JWT_TTL);
Map<String, Object> map = new HashMap<>();
map.put("alg", "HS256");
map.put("typ", "JWT");
JWTCreator.Builder jwtBuilder = JWT.create()
// 添加头部
.withHeader(map)
//超时设置,设置过期的日期
.withExpiresAt(expireDate)
//签发时间
.withIssuedAt(new Date());
// 构建 jwt 载荷
payload.forEach(jwtBuilder::withClaim);
// 签名,返回
return jwtBuilder.sign(Algorithm.HMAC256(DEFAULT_JWT_SECRET));
}
- 解析 jwt
/**
* 校验token并解析token
*
* @return 从token中解析出的载荷
*/
public static Map<String, Claim> verify(String token) {
if ("token_absent".equals(token)) {
// redis token 不存在
throw new UnauthorizedException();
}
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(DEFAULT_JWT_SECRET)).build();
DecodedJWT jwt = verifier.verify(token);
return jwt.getClaims();
} catch (TokenExpiredException e) {
log.error("jwt token已过期");
throw new UnauthorizedException();
} catch (JWTVerificationException e) {
log.error("jwt token不存在或不正确");
throw new ForbiddenException();
}
}