springSecurity主要完成认证和授权,认证就是谁可以访问,授权就是访问者可以干什么,我说一下登录的认证流程吧
1.前端发送请求携带用户名和密码访问登录接口
2.校验密码和数据库是否一致
3.如果一致,使用用户名生成一个token返回给前端
4.前端进行存储token,如果此时访问其他请求需要在请求头中携带token,服务器获取token进行解析是否有效,如果有效根据该token获取用户的信息
2.使用的依赖以及技术SpringSecurity+MybatisPuls+JWT+Redis+Mysql
项目地址:springSecurity2: springSecurity前后端分离+全局异常捕获+token挤掉线+token续期+动态资源授权 - Gitee.com
前端项目地址:springsecurityWeb: 前端页面
演示地址:springSecurity演示 登录->token挤掉线->认证->动态授权_哔哩哔哩_bilibili
本篇主要是介绍项目中的功能都写在了哪些类中方便读者查找
1.认证+token挤掉线+token续期
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
public JwtAuthenticationTokenFilter(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if(Objects.isNull(token)){
filterChain.doFilter(request,response);
return;
}
String userId = null;
try {
//校验token是否有效
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
WebExceptionUtil.authenticateException("token非法",CommonStatus.EXCEPTION);
}
//从redis中获取
String redisKey="loginUser:"+userId;
LoginUser loginUser= (LoginUser) redisTemplate.opsForValue().get(redisKey);
if(Objects.isNull(loginUser)){
WebExceptionUtil.authenticateException("用户未登录",CommonStatus.EXCEPTION);
}
//挤掉token,判断是否是最新的token
if(!loginUser.getNewToken().equals(token)){
WebExceptionUtil.authenticateException("其他地方已登录,请从新登录",CommonStatus.EXCEPTION);
}
//token续期
Long expire = redisTemplate.opsForValue().getOperations().getExpire(redisKey);
if(expire==-2){
WebExceptionUtil.authenticateException("token已过期,请从新登录",CommonStatus.EXCEPTION);
}else {
//过期时间不足10分钟,加1小时
if(expire<60*10){
redisTemplate.opsForValue().set(redisKey,loginUser,60*60, TimeUnit.SECONDS);
}
}
//存入SecurityContextHolder为了让其他拦截器能识别已经认证通过
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request,response);
}
}
认证:判断当前传入的token是否合法由JWT判断,如果token为空并且请求的地址非放行的地址,将会被Security内部拦截器处理并且返回403认证失败
token挤掉线:判断当前传入的token是否与数据库的token一致,如果不一致就是旧的token,因为新的token在每次用户登录的时候都会存储到redis中
token续期:我对jwt放弃了时间维护,直接把jwt的过期时间设置为永久,让redis来维护这个token的时间,当有请求过来时会判断当前的时间是否小于10分钟如果是,就将时间+1小时,等一小时过后没有新的请求访问,此时数据会被redis删除
动态资源授权
/**
* 动态权限校验
*/
public class AccessDeniedFilter extends OncePerRequestFilter {
private MenuDao menuDao;
public AccessDeniedFilter(MenuDao menuDao) {
this.menuDao = menuDao;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if(Objects.isNull(token)){
filterChain.doFilter(request,response);
return;
}
//获取数据库所有需要权限的菜单
List
动态权限授权:当有请求过来的时候会先把数据所有权限都查询出来放入list,
到list中遍历当前请求是否存在,如果找不到就说明当前的请求不需要权限即可访问,直接放行,
如果找到了证明是需要相应的权限才可访问,找到之后就获取当前用户的所持有权限放入list2中,
list2遍历当前请求是否存在,如果存在即有权限访问该请求,否则报403权限不足
全局Controller异常捕获
/**
* 处理Controller抛出异常会被该过滤器处理
*/
public class ControllerExceptionFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
try {
filterChain.doFilter(servletRequest, response);
} catch (Exception e) {
e.printStackTrace();
String causeStr="";
Throwable var1 = e.getCause();
if(var1 != null){
//获取更简洁的错误信息
Throwable cause = var1.getCause();
if(cause !=null){
causeStr=cause.toString();
causeStr=causeStr.replaceAll("\r\n","↓");
}
}
String stackTraceInfo = new StackTraceInfo().stackTraceInfo(e);
ResultJson resultJson = new ResultJson(500,var1==null?"":var1.getMessage(),stackTraceInfo);
WebUtils.renderString((HttpServletResponse) response, JSON.toJSONString(resultJson));
}
}
}
在WebSecurityConfig 类下配置
//Controller异常全局捕获
http.addFilterAfter(new ControllerExceptionFilter(), FilterSecurityInterceptor.class);
我自定义全局异常放到Security最后一个拦截器的后面,也就是说Security框架的拦截器全部走完没有发生错误,
就走我自定义的拦截器,如果Security某个拦截器发生了错误,那么错误就交给Security拦截器自己来处理,
当Security没有错误走到我的拦截器正常的访问了Controller,
如果Controller发生了异常就要往往抛才能被我自定义的拦截器捕获到,
Controller抛的异常不会被Security拦截器捕获是因为Controller上一层拦截器是我自定义的全局拦截器,我自定拦截器上一层才是Security
拦截器结构图
执行流程图
验证码
@RestController
public class CommonController {
@RequestMapping("/vc")
public void createVerifyCode(HttpServletRequest request, HttpServletResponse response){
VerifyCodeUtil verifyCodeUtil = new VerifyCodeUtil();
verifyCodeUtil.createVerifyCode(request,response);
}
}
前端明文加密
前端加密代码,需要导入jsencrypt.min.js文件
var encryptor = new JSEncrypt();
encryptor.setPublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCBJzjaOJCvy3hciAasAT9H47d/ql4U4b45Uvj5cUpqoxMIy1znrQKFagmbXNdXzpjwElpq6Ve7+JpqQsjTnzXI4ahn8Z2nfQ6TfoiOCy/aCH2dnRhyl9XcetZNfxfCN7dTibj8Ik6b2Th3iEiiBngHU3NxAk6Se11mk7rsCQnR+QIDAQAB");
$("#form-login").submit(function() {
$("[name='username']").prop("value",encryptor.encrypt($("[name='username']")[0].value));
$("[name='password']").prop("value",encryptor.encrypt($("[name='password']")[0].value));
});
后端解密代码
public class RSAUtils{
/**
* 解密
* @param str
* @return
*/
public static String decode(String str) {
return decrypt(str);
}
}