本文场景: 使用Spring Security做权限控制,系统包含普通用户和系统管理员两种类型,希望有不同的登录入口;并且在Spring Gateway上配置,Gateway使用的是WebFlux
,无法兼容MVC
,所以使用WebFlux配置Security。纯记录,目前项目还是小demo,贴的示例代码可以正常运行,但是业务逻辑还有点问题。
1. SecurityWebFilterChain配置
-
创建两个
SecurityWebFilterChain
bean,管理员配置限制范围为/manage/**
,用户将该路径设置为白名单,权限配置不限制范围。 -
管理员配置必须指定比用户配置更小的
@Order
,使其具有更高的优先级,能够在用户的SecurityWebFilterChain
之前被执行,因为一旦匹配到用户的SecurityWebFilterChain
,将不会继续匹配到管理员的配置,管理员的配置将不会生效。具体原因上一个节有分析。
package com.nineya.rkproblem.config;
import com.nineya.rkproblem.filter.ManageAuthenticationFilter;
import com.nineya.rkproblem.security.AuthenticationFailureHandler;
import com.nineya.rkproblem.security.AuthenticationSuccessHandler;
import com.nineya.rkproblem.security.ManageAuthenticationManager;
import com.nineya.rkproblem.security.UserAuthenticationManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
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;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import javax.annotation.Resource;
import java.util.LinkedList;
/**
* @author 殇雪话诀别
* 2020/11/7
*/
@Configuration
@EnableWebFluxSecurity
public class SecurityWebFluxConfiguration {
/**
* 登录成功的处理器
*/
@Resource
private AuthenticationSuccessHandler successHandler;
/**
* 登录失败的处理器
*/
@Resource
private AuthenticationFailureHandler failureHandler;
@Resource
private UserAuthenticationManager userAuthenticationManager;
@Resource
private ManageAuthenticationManager manageAuthenticationManager;
@Resource
private ManageAuthenticationFilter manageAuthenticationFilter;
/**
* 白名单
*/
private static final String[] AUTH_WHITELIST = new String[]{"/manage/login", "/user/login"};
@Bean
@Order(0)
public SecurityWebFilterChain manageSecurityFilterChain(ServerHttpSecurity http){
http
.securityMatcher(ServerWebExchangeMatchers.pathMatchers("/manage/**"))
.formLogin(n->n
.loginPage("/manage/login")
.requiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers("/manage/auth/login"))
.authenticationManager(manageAuthenticationManager)
.authenticationSuccessHandler(successHandler)
.authenticationFailureHandler(failureHandler))
.logout(n->n.logoutUrl("/manage/logout"))
.csrf(n->n.disable())
.httpBasic(n->n.disable())
.authorizeExchange(n->n
.pathMatchers(AUTH_WHITELIST).permitAll()
.pathMatchers("/manage/api/**").hasRole("MANAGE")
.anyExchange().authenticated())
.addFilterBefore(manageAuthenticationFilter, SecurityWebFiltersOrder.FORM_LOGIN);
return http.build();
}
@Bean
@Order(1)
public SecurityWebFilterChain userSecurityFilterChain(ServerHttpSecurity http){
http
.securityMatcher(ServerWebExchangeMatchers.anyExchange())
.formLogin(n->n
.loginPage("/user/login")
.requiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers("/user/auth/login"))
.authenticationManager(userAuthenticationManager)
.authenticationSuccessHandler(successHandler)
.authenticationFailureHandler(failureHandler))
.logout(n->n.logoutUrl("/user/logout"))
.csrf(n->n.disable())
.httpBasic(n->n.disable())
.authorizeExchange(n->n
.pathMatchers("/manage/**", "/user/login").permitAll()
.pathMatchers("/user/api/**").hasRole("USER"));
return http.build();
}
@Bean
public PasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
2. AuthenticationManager配置
两个登录入口需要创建两个AuthenticationManager
,本文只是简单示例,能正常跑起来,逻辑不一定通的哦。
package com.nineya.rkproblem.security;
import com.nineya.rkproblem.service.ManageDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
/**
* @author 殇雪话诀别
* 2020/11/15
*/
@Component
@Primary
public class ManageAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {
private Scheduler scheduler = Schedulers.boundedElastic();
@Autowired
private ManageDetailsServiceImpl manageDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
String username = authentication.getName();
String password = String.valueOf(authentication.getCredentials());
return retrieveUser(username)
.publishOn(scheduler)
.filter(u -> passwordEncoder.matches(password, passwordEncoder.encode(u.getPassword())))
.switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("账号或密码错误!"))))
.map(u-> new UsernamePasswordAuthenticationToken(u, u.getPassword()));
}
@Override
protected Mono<UserDetails> retrieveUser(String username) {
return manageDetailsService.findByUsername(username);
}
}
package com.nineya.rkproblem.security;
import com.nineya.rkproblem.service.ManageDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
/**
* @author 殇雪话诀别
* 2020/11/8
*/
@Component
public class UserAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {
private Scheduler scheduler = Schedulers.boundedElastic();
@Autowired
private ManageDetailsServiceImpl manageDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
String username = authentication.getName();
String password = String.valueOf(authentication.getCredentials());
return retrieveUser(username)
.publishOn(scheduler)
.filter(u -> passwordEncoder.matches(password, passwordEncoder.encode(u.getPassword())))
.switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
.map(u-> new UsernamePasswordAuthenticationToken(u, u.getPassword()));
}
@Override
protected Mono<UserDetails> retrieveUser(String username) {
return manageDetailsService.findByUsername(username);
}
}
3. 用户信息服务
在AuthenticationManager
中需要指定一个ReactiveUserDetailsService
用于查询用户信息,从而比对用户的登录数据是否正确,实现如下:
package com.nineya.rkproblem.service;
import com.nineya.rkproblem.client.ManageClient;
import com.nineya.rkproblem.entity.Manage;
import com.nineya.tool.text.CheckText;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
/**
* @author 殇雪话诀别
* 2020/11/8
*/
@Component
public class ManageDetailsServiceImpl implements ReactiveUserDetailsService {
@Autowired
private ManageClient manageClient;
@Override
public Mono<UserDetails> findByUsername(String username) {
if (!CheckText.checkMail(username)){
return Mono.error(new UsernameNotFoundException(String.format("邮箱 %s 格式错误!", username)));
}
Manage manage = manageClient.login(username);
if (manage == null){
return Mono.error(new UsernameNotFoundException(String.format("账号 %s 不存在!", username)));
}
UserDetails user = User.withUsername(username)
.password(manage.getPassword())
.roles("MANAGE")
.build();
return Mono.just(user);
}
}
4. 登录结果处理器
在本文场景中,登录结果处理和业务无关,所以管理员和用户共用一个处理器。
登录失败处理器:
package com.nineya.rkproblem.security;
import com.alibaba.fastjson.JSONObject;
import com.nineya.rkproblem.entity.ResponseResult;
import com.nineya.rkproblem.entity.ResultCode;
import org.springframework.core.io.buffer.DataBuffer;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* @author 殇雪话诀别
* 2020/11/12
*/
@Component
public class AuthenticationFailureHandler implements ServerAuthenticationFailureHandler {
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
ResponseResult result = new ResponseResult().setError(true);
if (exception instanceof UsernameNotFoundException) {
// 用户不存在
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage(exception.getMessage());
} else if (exception instanceof BadCredentialsException) {
// 密码错误
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage(exception.getMessage());
} else if (exception instanceof LockedException) {
// 用户被锁
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage(exception.getMessage());
} else {
// 系统错误
result.setCode(ResultCode.SERVER_ERROR.getCode());
result.setMessage(exception.getMessage());
}
String body = JSONObject.toJSONString(result);
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
登录成功处理器,在此处可实现通过Authentication
中的信息生成token响应给客户端,实现自定义token,可以关闭Security默认的Session方案。但是如果此处自定义token,需要添加一个token验证的过滤器,在每次请求时解析自定义的token创建Authentication
。
package com.nineya.rkproblem.security;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.nineya.rkproblem.entity.ResponseResult;
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.ServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.stream.Collectors;
/**
* @author 殇雪话诀别
* 2020/11/12
*/
@Component
public class AuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
response.setStatusCode(HttpStatus.OK);
Algorithm algorithm = Algorithm.HMAC256("secret");
String token = JWT.create()
.withClaim("uid", "123456")
.withIssuedAt(new Date())
.withIssuer("rkproblem")
.sign(algorithm);
JWTVerifier verifier = JWT.require(algorithm)
.build();
DecodedJWT jwt = verifier.verify(token);
System.out.println(jwt.getClaims().entrySet().stream()
.map(n->n.getKey()+" = " + n.getValue().asString()).collect(Collectors.joining(", ")));
ResponseResult<String> result = ResponseResult.success(token);
String body = JSONObject.toJSONString(result);
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
5. 权限认证过滤器
示例中,简单的打印了一下token,并未将token转换为AuthenticationToken对象。
package com.nineya.rkproblem.filter;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
/**
* 将表单里的参数转为AuthenticationToken对象
*
* @author 殇雪话诀别
* 2020/11/20
*/
@Component
public class ManageAuthenticationFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain) {
HttpHeaders headers = serverWebExchange.getRequest().getHeaders();
String manageToken = headers.getFirst("MANAGE_TOKEN");
System.out.println(manageToken);
return webFilterChain.filter(serverWebExchange);
}
}