SpringBoot-SpringSecurity结合JWT的⾝份验证及动态权限解决⽅案
花了点时间写了⼀个SpringSecurity集合JWT完成⾝份验证的Demo,并按照⾃⼰的想法完成了动态权限问题。在写这个Demo之初,使⽤的是SpringSecurity⾃带的注解权限,但是这样权限就显得不太灵活,在实现之后,感觉也挺复杂的,欢迎⼤家给出建议。Demo下载地址,见⽂末。
JWT是什么可以参考我另⼀篇博⽂:
认证流程及授权流程
我画了个建议的认证授权流程图,后⾯会结合代码进⾏解释整个流程。
⼀、登录认证阶段
实现SpringSecurity的UrnamePasswordAuthenticationFilter接⼝(public class TokenLoginFilter extends UrnamePasswordAuthenticationFilter),在它的实现类的构造⽅法⾥设置登录的请求路径和请求⽅式。
this.tPostOnly(fal);
// 认证路径 - 发送什么请求,就会进⾏认证
this.tRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/rvice_auth/admin/index/login","POST"));
当前端发起配置的请求时,请求会被拦截,进⼊到attemptAuthentication⽅法进⾏验证,在这个⽅法⾥可以从request中取出账号、密码,从⽽调⽤AuthenticationManager的authenticate去校验账号、密码是否正确。
@Override
在职mba培训班public Authentication attemptAuthentication(HttpServletRequest request, HttpServletRespon respon)
throws AuthenticationException {
try{
Ur ur =new ObjectMapper().InputStream(), Ur.class);
// 也可以直接获取账号密码
String urname =obtainUrname(request);
String password =obtainPassword(request);
log.info("TokenLoginFilter-attemptAuthentication:尝试认证,⽤户名:{}, 密码:{}", urname, password);
// 在authenticate⾥去进⾏校验的,校验过程中会去把UrDetailService⾥返回的SecurityUr(UrDetails)⾥的账号密码和这⾥传的账号密码进⾏⽐对// 并在UrDetailService⾥将权限进⾏赋予
// 校验通过,会进⼊到successfulAuthentication⽅法
return authenticationManager.authenticate(new Urname(), ur.getPassword(),new ArrayList<>() ));
}catch(IOException e){
throw new RuntimeException(e);
}
}
那么这个authenticate⽅法是怎么验证我们账号密码正确性的呢?
打上断点,跟随源码,我们进⼊到authenticate⽅法内部:
然后进⼊这个⽅法内部,继续往下⾛,看到⼀段核⼼代码:
进⼊retrieveUr⽅法⾥,然后往下⾛,看到⼀句核⼼代码,这个核⼼代码就是获取⽤户信息的:
这⾥注意,调⽤了UrDetailsService的loadUrByUrname⽅法,传⼊的就是前端传过来的urname,意思就是要根据这个urname去获取UrDetails对象,所以我们就要去查询数据库,所以我们就要实现UrDetailsService接⼝并重写loadUrByUrname⽅法。
@Service("urDetailsService")
@Slf4j
public class UrDetailServiceImpl implements UrDetailsService {
@Override
public UrDetails loadUrByUrname(String urname)throws UrnameNotFoundException {
log.info("根据urname去数据库查询⽤户信息,urname:{}", urname);
// 1、从数据库中取出⽤户信息 - 这⾥模拟,直接new⼀个Ur对象
bothof
Ur ur =new Ur();
ur.tUrname(urname);
// 111111经过加密后
ur.tPassword("96e79218965eb72c92a549dd5a330112");
SecurityUr curityUr =new SecurityUr(ur);
// 可以根据查出来的ur.getId()去查询这个⽤户对应的权限集合 - 这⾥模拟,直接new⼀个结合
List<String> authorities =new ArrayList<>();
// 将权限赋予⽤户
curityUr.tPermissionValueList(authorities);
return curityUr;
}
}
在这个⽅法⾥,我们通过查询数据库,获取⽤户urname、password和其对应的权限并设置到UrDetails对象⾥(代码⾥的SecurityUr是我⾃⼰implements UrDetails的,也就是它的⼦类)。gotham
获取到urDetails对象后,回到之前的代码(retriveUr所在的地⽅)⾥,这个ur经过包装,⾥⾯包含我们从数据库⾥取出
的urname、password。
接着往下看,看到核⼼代码:
注意这个additionalAuthenticationChecks⽅法,我们进⼊到这个⽅法内部:
可以发现,这是对⽐密码的,即前端传过来的密码和数据库中存储的已经加密过的密码是否能匹配上。然后我们回到之前的代码⾥,直接到结尾,返回⼀个对象。
在账号、密码验证完之后的⼀系列操作⾥,SpringSecurity⾃⼰再对数据进⾏⼀些封装放到SecurityContextHolder⾥。
⾄此,⽤户的认证流程已经⾛完。
认证成功之后
认证成功之后,我们要告诉前端登录认证通过,会进⼊UrnamePasswordAuthenticationFilter的successfulAuthentication⽅法⾥。
americanliterature
/**
* 登录成功
圣光调息* @param request request
* @param respon respon
* @param chain chain
* @param auth auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletRespon respon, FilterChain chain,
Authentication auth)throws IOException, ServletException {
log.info("TokenLoginFilter-successfulAuthentication:认证通过!");
SecurityUr ur =(SecurityUr) Principal();
// 创建token
String token = CurrentUrInfo().getUrname());
log.info("创建的Token为:{}", token);
星期的拼音// 这⾥建议,以urname为Key,权限集合为value将权限存⼊Redis,因为权限在后⾯会频繁被取出来⽤
// redisTemplate.opsForValue().CurrentUrInfo().getUrname(), ur.getPermissionValueList());
// 响应给前端调⽤处
ResponUtil.out(respon, ResponResult.ok().data("token", token));
}
在这个⽅法⾥,我们创建⼀个token,并相应给前端调⽤者。ResponUtil是封装的⼀个响应⼯具,to
kenManager是JWT⼯具,这⾥不做过多解释,可以去仓库克隆我的源码查看,根据我这个流程⾛即可。
因为我这个Demo是基于前后端分离的,因此只需响应给前端结果(⽐如这⾥的token)即可,让前端来跳转。如果不是前后端分离的,可以在这⾥进⾏页⾯跳转。
如果认证失败
认证失败,会进⼊UrnamePasswordAuthenticationFilter的unsuccessfulAuthentication⽅法⾥。
/**
* 登录失败
* @param request
* @param respon
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletRespon respon,
AuthenticationException e)throws IOException, ServletException {
log.info("TokenLoginFilter-unsuccessfulAuthentication:认证失败!");
// 响应给前端调⽤处
ResponUtil.out(respon, ());
}
在这个⽅法⾥,直接响应给前端错误情况即可。因为我这个Demo是基于前后端分离的,因此只需响应给前端结果、状态码即可,让前端来跳转。如果不是前后端分离的,可以在这⾥进⾏页⾯跳转。
⼆、授权阶段 - 如果你要做权限控制
继承BasicAuthenticationFilter类,重写doFilterInternal过滤器,在这个过滤器⾥获取token并验证,并进⾏动态权限控制。
@Slf4j
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private AntPathMatcher antPathMatcher =new AntPathMatcher();
public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager){
super(authManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletRespon respon, FilterChain chain)
throws IOException, ServletException {
UrnamePasswordAuthenticationToken authentication = null;
try{
log.info("授权过滤器,验证");
authentication =getAuthentication(request);
}catch(ExpiredJwtException e){
// 可能token过期
log.info("异常捕获:{}",e.getMessage());
ResponUtil.out(respon, ResponResult.unauthorized());
}
if(authentication != null){
String url = RequestURI();
// tAuthentication设置不设置都⾏,如果需要⽤注解来控制权限,则必须设置
UrServiceImpl urService =new UrServiceImpl();
List<Menu> menuList = AllMenus();
// 遍历所有菜单
for(Menu menu : menuList){
// 如果url匹配上了
if(antPathMatcher.Pattern(), url)&& Roles().size()>0){intec
log.info("URL匹配上了,请求URL:{},匹配上的URL:{}", url, Pattern());
List<String> stringList =new ArrayList<>();
for(GrantedAuthority authority : Authorities()){
String authority1 = Authority();
stringList.add(authority1);
}
for(Role role : Roles()){
Name())){
batch是什么意思log.info("⾓⾊匹配,⾓⾊为:{}", Name());
log.info("⾓⾊匹配,⾓⾊为:{}", Name());
chain.doFilter(request, respon);
return;
}
太原英语学校
}
// 没有权限
log.info("URL匹配上了,但⽆权访问,请求URL:{},匹配上的URL:{}", url, Pattern());
ResponUtil.out(respon, Permission());
return;
}
}
// url没有匹配上菜单,可以访问
log.info("URL未匹配上,所有⼈都可以访问!");
chain.doFilter(request, respon);
}el{
// 没有登录
log.info("⽤户Token⽆效!");
}
}
private UrnamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){
// token置于header⾥
String token = Header("X-Token");
log.info("X-Token:{}", token);
if(token != null &&!"".im())){
/
/ 根据token获取⽤户名
String urName = UrFromToken(token);
// 这⾥可以根据⽤户名去Redis中取出权限集合
// 不应该从SecurityContextHolder获取,会出现问题,如果你换⼀个token(这个token也是有效的)来调⽤⽅法,从这⾥取,这权限还是之前token登录时存进来的(经过我测试)
// 为什么呢?我的猜测是:因为JWT是⽆状态的,你没有办法在注销的时候,将SpringSecurity全局对象⾥的东西清理
// 如果你先⽤账号2登录获取⼀个token2,然后⽤账号1登录获取⼀个token1,⽤token1去调⽤⼀次api的时候从SecurityContextHolder获取⼀次权限,然后⽤token2去调⽤⼀次api获取⼀次权限,你会发现这个权限居然是token1拥有的(我测试过)
// Authentication authentication = Context().getAuthentication();
// List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(urName);
// 这⾥直接模拟从Redis中取出权限
List<String> permissionValueList =new ArrayList<>();
// 权限 - 为了测试根据权限控制访问权限
permissionValueList.add("st");
// ⾓⾊ - 为了测试根据⾓⾊控制访问权限
permissionValueList.add("ROLE_admin");
// 需要将权限转换成SpringSecurity认识的
Collection<GrantedAuthority> authorities =new ArrayList<>();
for(String permissionValue : permissionValueList){
if(StringUtils.isEmpty(permissionValue)){
continue;
}
SimpleGrantedAuthority authority =new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
if(!StringUtils.isEmpty(urName)){
motorway
log.info("授权过滤器:授权完成!");
return new UrnamePasswordAuthenticationToken(urName, token, authorities);
}
return null;
}
return null;
}
}
如果不做动态权限,则可以省略那⼀部分对⽐url的代码。但是⽤户的拥有的权限,建议存储在Redis⾥。每次进⼊到这个过滤器,就将其取出来封装成Security认识的,放到SecurityContextHolder⾥,这个时候你的权限是定死了的,可以在配置⽂件⾥进⾏配置,也可以使⽤注解在Controller⾥进⾏控制。