[多登录页]Spring boot集成Spring security及JWT实现多页面(多种登录方式)前后端分离登录鉴权
前言
我的这篇文章详细描述了如何使用Spring boot集成Spring security及JWT实现单一登录页面前后端分离登录鉴权。但是在服务设计中,经常有多端登录的需求。比如一套系统一般分为后台管理系统,和前台系统。更细致区分,可能前台还分为不同登录端,比如移动端和PC端。这时,不同的系统就需要不同的登录鉴权体系。比如admin的登录鉴权无论是颁发的token还是使用的用户存储表,都与user前台的数据不同。这就要求我们在使用Spring security的时候,具备提供多套登录鉴权认证体系的能力。如果对此问题比较感兴趣的兄弟,请继续看下文
文中整套demo的构建目标如下:
- 前后端分离系统,没有使用thymeleaf等模板引擎,所以只提供api接口,没有做页面跳转。
- admin登录form提交位置为/admin/login,user登录form提交位置为/user/login。
- admin和user通过不同表存取用户信息。
最后说一句啊,由于现在基本都是微服务架构了,微服务下的服务认证体系,最好还是用Oauth2.0搞认证鉴权中心才是正途,然后Oauth2.0如果要实现一个认证中心多个类型的账户认证,这种方案也可以用
基础配置(与单一页面配置方案相同)
依赖配置
<dependencies>
<!-- web -->
<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>
<!--JWT(Json Web Token)登录支持-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
</dependencies>
<!--版本管理-->
<dependencyManagement>
<dependencies>
<!--SpringBoot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.7.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
yml配置
# JWT
jwt:
# JWT存储的请求头
tokenHeader: Authorization
# JWT 解密加密使用的密钥
salt: web-secret
# JWT的超期限时间(30*60*24)
expiration: 604800
# JWT 负载中拿到开头
tokenHead: Bearer
JWT配置
引入JWT工具类
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken生成的工具类
*/
@Component
public class JwtTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.salt}")
private String salt;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 根据claims生成token
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims) // 设置负载
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) // 过期时间,当前时间 + 过期时间
.signWith(SignatureAlgorithm.HS512, salt) // 签发算法及秘钥
.compact();
}
/**
* 根据userDetails生成token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 验证token是否还有效
*
* @param token 客户端传入的token
* @param userDetails 从数据库中查询出来的用户信息
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 从token中获取claims
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(salt)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败:{}",token);
}
return claims;
}
/**
* 从token中获取loginname
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判断token是否已经失效
*/
private boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}
/**
* 从token中获取过期时间
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 判断token是否可以被刷新
*/
public boolean canRefresh(String token) {
return !isTokenExpired(token);
}
/**
* 刷新token
*/
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
至此以上三部分都与单一页面认证无异
核心配置
原理
由于spring security没有单独考虑过多页面登录方案。比如,单一页面登录逻辑的实现,是通过自定义serDetailsService方案实现的,但是这种方案自定义化程度低,相当于只把真实用户数据的读取自定义能力开放给了使用者,因此这种方式不适合用于改写多端登录。所以如果要实现多页面登录,要自定义一套登录逻辑,本文使用的是Filter+Manager+Provider+Token的方案实现,涉及到部分源码的改写,但是不难,大家跟着注释能走下来。简单描述下这几个东西的作用:
-
Filter负责拦截请求,并调用Manager的authenticate方法完成认证
spring security本身就是通过一系列filter实现登录鉴权的拦截认证的。所以要新加一套自定义的登录认证体系,首先要加一个Filter做为拦截入口
-
Manager负责管理多个Provider,并选择合适的Provider进行认证。Manager的authenticate方法实际上是调用Provider中authenticate方法执行的
这里不做详细解释,详细源码解析在这里
-
Provider负责具体authenticate逻辑的处理,检验账号密码等操作。
Provider下面有许多具体实现类,有一个专门封装好了的实现类叫AbstractUserDetailsAuthenticationProvider,可以基于此类改写登录认证逻辑。
-
Token是认证信息,包含账号密码。
filter、manager、provider之间一直流转的账号信息封装类,是UsernamePasswordAuthenticationToken,用于封装用于认证的用户信息。这里和UserDetails做个区分,UsernamePasswordAuthenticationToken中一直封装的是被认证信息,而UserDetails中封装的则是从用户数据库中读取的,真实用户信息。
关于spring security中各个filter的介绍,建议参考这篇文章
废话不多说,开整
登录认证流程
普通request -> 全部拦截到JwtAuthenticationTokenFilter完成Token登录鉴权,如果没有携带Token,后续有两种可能
① 如果uri是/admin/login或/user/login,将请求拦截到CustomFilter进行账号密码认证。
② 如果uri不是/admin/login或/user/login,请求放行,进行后续处理。
配置controller,用于测试效果
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class TestController {
@ResponseBody
@GetMapping("/hello")
public String hello(){
return "hello spring sercurity";
}
@ResponseBody
@GetMapping("/admin/hello")
public String adminHello(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "hello admin:" + authentication.getPrincipal();
}
@ResponseBody
@GetMapping("/user/hello")
public String userHello(){
return "hello user";
}
}
配置MyUserService用于模拟真实用户数据存取
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 模拟不同类型用户数据
*/
@Component
public class MyUserService {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 模拟admin数据库获取数据
* @return
*/
public UserDetails adminUserDetails(){
return User.withUsername("admin")
.password(passwordEncoder.encode("123456"))
.roles("ADMIN")
.build();};
/**
* 模拟user数据库获取数据
* @return
*/
public UserDetails userUserDetails() {
return User.withUsername("user")
.password(passwordEncoder.encode("654321"))
.roles("USER")
.build();
}
}
配置JwtAuthenticationTokenFilter进行带token request鉴权认证
import com.example.testproject.util.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* JWT过滤器
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private MyUserService myUserService;
/**
* 完成带token的request的解析和鉴权
*
* @param request
* @param response
* @param chain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
// token非空的情况,解析出username
if (StringUtils.hasLength(authHeader) && authHeader.startsWith(this.tokenHead)) {
// token去头
String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
// 解析username
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("user token checking:{}", username);
// 验证token合法性,SecurityContextHolder.getContext().getAuthentication()用于验证此request是否在前序已经通过了验证
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 根据login uri拿到用哪张表鉴权,获取对比对象
UserDetails userDetails = null;
if (request.getRequestURI().startsWith("/admin/")){
userDetails = myUserService.adminUserDetails();
}else if (request.getRequestURI().startsWith("/user/")){
userDetails = myUserService.userUserDetails();
}else{
userDetails = myUserService.userUserDetails();
}
// 使用jwt signature验证token真实性
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
// 验证成功,组装UsernamePasswordAuthenticationToken对象,放入SecurityContextHolder中,表示鉴权成功
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
LOGGER.info("user token success:{}", username);
} else {
// token验证不成功
LOGGER.info("user token failed:{}", username);
throw new BadCredentialsException("Bad credentials");
}
} else {
// username为空,或前序filter已经验证通过此request
LOGGER.info("user token pass:{}", username);
}
}else{
// token为空的情况,交给后续CustomFilter等其他filter,做账号密码处理
}
chain.doFilter(request, response);
}
}
多页面登录的JwtAuthenticationTokenFilter与单页面登录的配置有所差别,主要差别在于解析token,并根据token进行登录认证上。单页面登录场景下,UserDetails userDetails = userDetailsService.loadUserByUsername(username);
。多页面场景下,userDetails 要根据request的uri去判断从哪个表取值
配置Filter 进行不带token request(基于账号密码)鉴权认证
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 全盘照抄UsernamePasswordAuthenticationFilter,只改了2处
* 1. 构造方法允许自定义url
* 2. setDetails改为向UsernamePasswordAuthenticationToken中存储request数据,用于provider中根据uri确定查哪个表
*/
public class CustomFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/user/login",
"POST");
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public CustomFilter(String path) {
super(new AntPathRequestMatcher(path, "POST"));
}
public CustomFilter(String path, AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher(path, "POST"), authenticationManager);
}
/** 以下为全盘照抄 UsernamePasswordAuthenticationFilter **/
/**
* detail放入request
* @param request
* @param authRequest
*/
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(request);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* Enables subclasses to override the composition of the password, such as by
* including additional values and a separator.
* <p>
* This might be used for example if a postcode/zipcode was required in addition to
* the password. A delimiter such as a pipe (|) should be used to separate the
* password and extended value(s). The <code>AuthenticationDao</code> will need to
* generate the expected password in a corresponding manner.
* </p>
* @param request so that request attributes can be retrieved
* @return the password that will be presented in the <code>Authentication</code>
* request token to the <code>AuthenticationManager</code>
*/
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
/**
* Enables subclasses to override the composition of the username, such as by
* including additional values and a separator.
* @param request so that request attributes can be retrieved
* @return the username that will be presented in the <code>Authentication</code>
* request token to the <code>AuthenticationManager</code>
*/
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
/**
* Sets the parameter name which will be used to obtain the username from the login
* request.
* @param usernameParameter the parameter name. Defaults to "username".
*/
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
/**
* Sets the parameter name which will be used to obtain the password from the login
* request..
* @param passwordParameter the parameter name. Defaults to "password".
*/
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
/**
* Defines whether only HTTP POST requests will be allowed by this filter. If set to
* true, and an authentication request is received which is not a POST request, an
* exception will be raised immediately and authentication will not be attempted. The
* <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
* authentication.
* <p>
* Defaults to <tt>true</tt> but may be overridden by subclasses.
*/
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.usernameParameter;
}
public final String getPasswordParameter() {
return this.passwordParameter;
}
}
原计划使用UsernamePasswordAuthenticationFilter做修改,但是发现UsernamePasswordAuthenticationFilter的ANT_PATH_REQUEST_MATCHER变量给了个final值,说明作者不希望有人继承修改这个类,因此,向上找到了AbstractAuthenticationProcessingFilter这个类。但是由于此次也是要实现基于账号密码登录的认证,为了防止部分属性遗漏,还是copy了UsernamePasswordAuthenticationFilter类的实现,只改了2个地方
- 构造方法允许自定义url
- setDetails改为向UsernamePasswordAuthenticationToken中存储request数据,用于provider中根据uri确定查哪个表
配置MultiUserProvider用于实现具体用户认证逻辑
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class MultiUserProvider extends AbstractUserDetailsAuthenticationProvider {
@Autowired
private MyUserService myUserService;
/**
* 存储用户资源获取
* @param username
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 获取filter中存放的request
HttpServletRequest request = (HttpServletRequest) authentication.getDetails();
String uri = request.getRequestURI();
// 根据路径匹配具体使用哪套账号密码
UserDetails userDetails = null;
if (uri.startsWith("/admin")){
userDetails = myUserService.adminUserDetails();
}else if(uri.startsWith("/user")){
userDetails = myUserService.userUserDetails();
}
return userDetails;
}
/**
* 凭据检查
* 参考https://blog.csdn.net/dengdeying/article/details/103678030 源码详解
* @param userDetails
* @param authentication
* @throws AuthenticationException
*/
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
boolean username = authentication.getPrincipal().equals(userDetails.getUsername());
boolean passowrd = passwordEncoder.matches((String) authentication.getCredentials(),userDetails.getPassword());
if (!username || !passowrd){
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
配置SecurityConfig,实现spring security全局控制
import cn.hutool.json.JSONUtil;
import com.example.testproject.common.Result;
import com.example.testproject.common.ResultGenerator;
import com.example.testproject.component.JwtAuthenticationTokenFilter;
import com.example.testproject.component.CustomFilter;
import com.example.testproject.util.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* Spring Security配置
*/
@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationProvider MultiUserProvider;
@Autowired
private JwtTokenUtil jwtTokenUtil;
/**
* 全局配置:路径、csrf、session等功能启停
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// admin页面拦截配置
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.anyRequest()// 除上面外的所有请求全部需要鉴权认证
.authenticated()
// 登录配置
.and()
.formLogin().permitAll()
// 登出配置
.and()
.logout()
.logoutUrl("/admin/logout")
.permitAll()
// 无权限访问和访问失败
.and()
.exceptionHandling()
.accessDeniedHandler(new CustomAccessDeniedHandler())
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
// 自定义权限拦截器JWT过滤器
.and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(customFilter("/admin/login"), UsernamePasswordAuthenticationFilter.class)
.addFilterAt(customFilter("/user/login"), UsernamePasswordAuthenticationFilter.class);
// 跨域拦截放开
http.csrf().disable();
// 禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
/**
* 注册编解码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 处理成功配置
*/
class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 获取用户信息
User user = (User) authentication.getPrincipal();
// 生成token
String token = jwtTokenUtil.generateToken(user);
// 封装token
response.setHeader("token", token);
response.setHeader("Access-Control-Expose-Headers", "token");//暴露自定义回复头
Result result = ResultGenerator.genSuccessResult();
result.setData(token);
// 转JSON
String s = JSONUtil.toJsonStr(result);
// 输出respon
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.println(s);
writer.flush();
writer.close();
}
}
/**
* 处理失败配置
*/
class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 获取错误信息
Result result = ResultGenerator.genFailResult(exception.getMessage());
// 转JSON
String s = JSONUtil.toJsonStr(result);
// 输出respon
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.println(s);
writer.flush();
writer.close();
}
}
/**
* 权限不足拒绝访问配置
*/
class CustomAccessDeniedHandler implements AccessDeniedHandler{
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 获取错误信息
Result result = ResultGenerator.genFailResult(accessDeniedException.getMessage());
// 转JSON
String s = JSONUtil.toJsonStr(result);
// 输出respon
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.println(s);
writer.flush();
writer.close();
}
}
/**
* 未通过认证访问配置
*/
class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 获取错误信息
Result result = ResultGenerator.genFailResult(authException.getMessage());
// 转JSON
String s = JSONUtil.toJsonStr(result);
// 输出respon
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.println(s);
writer.flush();
writer.close();
}
}
/**
* uri过滤器,根据不同uri匹配不同规则
* @param path
* @return
* @throws Exception
*/
private CustomFilter customFilter(String path) throws Exception{
CustomFilter adminFilter = new CustomFilter(path);
adminFilter.setAuthenticationManager(authenticationManager());
//登录成功后跳转
adminFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
adminFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
return adminFilter;
}
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
return new JwtAuthenticationTokenFilter();
};
//核心:配置管理器
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//在管理器中添加provider
auth.authenticationProvider(MultiUserProvider).build();
}
}
这里面的代码和单页面登录逻辑差距不大,最重要的就三行代码。这里要注意jwtAuthenticationTokenFilter一定要放在所有认证filter的最前面,因为带token的请求一定要先过滤掉,其次才是admin和user的请求
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(customFilter("/admin/login"), UsernamePasswordAuthenticationFilter.class)
.addFilterAt(customFilter("/user/login"), UsernamePasswordAuthenticationFilter.class);
配置结果
可实现最终目标,使用postman发送请求,可观测到/admin/login和/user/login得到的登录结果token不同,各自的token只能够访问各自路径下的资源。比如/admin/login得到的token只能用于访问/admin/l下的资源,/user/login得到的token能用于访问/**下的资源,