springSecurity主要完成认证和授权,认证就是谁可以访问,授权就是访问者可以干什么,我说一下登录的认证流程吧
1.前端发送请求携带用户名和密码访问登录接口
2.校验密码和数据库是否一致
3.如果一致,使用用户名生成一个token返回给前端
4.前端进行存储token,如果此时访问其他请求需要在请求头中携带token,服务器获取token进行解析是否有效,如果有效根据该token获取用户的信息
2. 使用される依存関係とテクノロジーSpringSecurity+MybatisPuls+JWT+Redis+Mysql
プロジェクトアドレス: springSecurity2: springSecurity フロントエンド分離 + グローバル例外キャプチャ + トークンスクイーズライン + トークン更新 + 動的リソース認可 - Gitee.com
フロントエンドプロジェクトのアドレス: springsecurityWeb: フロントエンドページ
デモアドレス: springSecurity デモログイン -> トークンスクイーズライン -> 認証 -> 動的認可_哔哩哔哩_bilibili
この記事では、プロジェクト内の関数がどのクラスで記述されているかを主に紹介します。読者が見つけやすいようにするためです。
1. 認証 + トークンによる回線の絞り出し + トークンの更新
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
public JwtAuthenticationTokenFilter(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
private RedisTemplate<String,Object> 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<Menu> menuList = menuDao.selectList(null);
boolean competence=false;
for (Menu menu : menuList) {
String menuUrl=menu.getUrl();
boolean matches = Pattern.matches(menuUrl, request.getServletPath());
//当前url是否需要权限 true 需要 : false 不需要
if(matches){
competence=true;
break;
}
}
//当前请求不需要权限直接放行
if(!competence){
filterChain.doFilter(request,response);
return;
}
//以下是需要权限的处理
boolean denied=false;
Set<String> menus =getUserMenus();
for (String menuUrl : menus) {
//当前的url和数据库的url匹配
denied= Pattern.matches(menuUrl,request.getServletPath());
//true 匹配上 : false 未匹配
if(denied){
break;
}
}
if(!denied){
ResultJson resultJson = new ResultJson(403,"权限不足,无法访问");
WebUtils.renderString((HttpServletResponse) response, JSON.toJSONString(resultJson));
}else{
filterChain.doFilter(request,response);
}
}
/**
* 获取当前用户的所有权限
* @return
*/
private Set<String> getUserMenus(){
SysUserDTO sysUser = UserUtil.getUser();
List<SysRoleDTO> roles = sysUser.getRoles();
Set<String> menus= new HashSet<>();
for (SysRoleDTO role : roles) {
List<Menu> menuList = role.getMenuList();
for (Menu menu : menuList) {
menus.add(menu.getUrl());
}
}
return menus;
}
}
动态权限授权:当有请求过来的时候会先把数据所有权限都查询出来放入list,
到list中遍历当前请求是否存在,如果找不到就说明当前的请求不需要权限即可访问,直接放行,
如果找到了证明是需要相应的权限才可访问,找到之后就获取当前用户的所持有权限放入list2中,
list2遍历当前请求是否存在,如果存在即有权限访问该请求,否则报403权限不足
グローバルコントローラー例外キャプチャ
/**
* 处理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);
}
}