前两篇文章我们整合了shiro+jwt无状态权限验证,这样只把tocken存储在前端,后端只验证Tocken是否合法。
考虑一个问题就是我们在设置了token过期时间之后会使前端用户退出在APP端的话很不友好,而且也不是很安全。
两种业务模式仅供参考:
1.BS架构时 我需要前端在国半小时自动退出,那么我们可以直接使用tocken过期自动退出。
2.在app端我不想让用户经常退出,需要让他自己手动退出,那么问题来了,JWT是不能手动注销的,我们就需要用到了redis做存储,设置redis的失效时间长于自己定义的jwt的tocken过期时间,每次登陆时先清空该用户的tocken 然后创建新的tocken存储在redis里面,在Realm类里面验证自定义的tocken是否过期,过期之后去查找redis里面的tocken进行tocken验证。验证成功之后在创建一个新的token返回前端 重新给redis的token赋值,这样就不会出现 因为token过期而实效的问题了,当需要注销登录时直接清空前端token值和redis里面的token值就可以了。
问题来了:假设redis里面的token过期之后就没有新的tocken返回前端了 ,所以解决这个问题就用到了数据库,当redis里面的tocken过期之后去检查数据库然后更新数据库新的tocken和更新redis里面的新的tocken值返回给前端。
业务一: Realm类
package eurekaclientone.demo.shiro;
import eurekaclientone.demo.daoImpl.loginImpl;
import eurekaclientone.demo.jwt.jwtToken;
import eurekaclientone.demo.utils.RedisUtil;
import eurekaclientone.demo.utils.jwtUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashSet;
import java.util.Set;
@Component
public class CustomRealm extends AuthorizingRealm {
HttpServletRequest request;
HttpServletResponse response;
private loginImpl logins;
@Autowired
private void setUserMapper(loginImpl logins) {
this.logins = logins;
}
@Autowired
private RedisUtil redisUtil;
@Autowired
private eurekaclientone.demo.daoImpl.tockenImpl tockenImpl;
/**
* 必须重写此方法,不然会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof jwtToken;
}
/**
* 获取身份验证信息
* Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
*
* @param authenticationToken 用户身份信息 token
* @return 返回封装了用户信息的 AuthenticationInfo 实例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("————身份认证方法————");
String token = (String) authenticationToken.getCredentials();
// 解密获得username,用于和数据库进行对比
String username = jwtUtil.getUsername(token);
//首先先去判断自动生成的token是否已经过期 如果已经过期去redis里面判断是否过期
if(!jwtUtil.verify(token, username)){
if(redisUtil.hasKey(username)==false){
//如果redis里面也是空的提示重新登录 有可能tocken过期 就在数据库里面查询
if(StringUtils.isBlank(tockenImpl.selectToken(username))){
throw new AuthenticationException("您的登录已过期,请您重新登录!");
}else {
//判断数据库
if(StringUtils.isNotBlank(tockenImpl.selectToken(username))){
//如果数据库不是空的 判断该用户是否存在
String password = logins.getPassword(username);
if (password == null) {
throw new AuthenticationException("该用户不存在!");
}
//重新生成token 放进redis 每次请求方法返回给前端重置token
redisUtil.del(username);
tockenImpl.deTocken(username);
redisUtil.set(username,jwtUtil.createToken(username),360);
tockenImpl.inTocken(username,jwtUtil.createToken(username));
}
}
} else {
if(redisUtil.get(username).toString().equals(token)) {
String password = logins.getPassword(username);
if (password == null) {
throw new AuthenticationException("该用户不存在!");
}
}else{
throw new AuthenticationException("用户认证失败");
}
}
}
// 拿到视图解析器
/* if (username == null || !jwtUtil.verify(token, username)) {
throw new AuthenticationException("token认证失败!");
}*/
//如果自动生成的token不是空的 判断该用户是否存在
String password = logins.getPassword(username);
if (password == null) {
throw new AuthenticationException("该用户不存在!");
}
return new SimpleAuthenticationInfo(token, token, "MyRealm");
//从Shiro中获取用户
//UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
/* // 从数据库获取对应用户名密码的用户
String password = logins.getPassword(token.getUsername());
if (null == password) {
throw new AccountException("用户名不正确");
} else if (!password.equals(new String((char[]) token.getCredentials()))) {
throw new AccountException("密码不正确");
}
return new SimpleAuthenticationInfo(token.getPrincipal(), password, getName());*/
}
/**
* 获取授权信息
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("————权限认证————");
//单独shiro认证时
/* String username = (String) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获得该用户角色
String role = logins.getRole(username);
//如果一个用户有多个权限应该是List接受循环遍历
Set<String> set = new HashSet<>();
//需要将 role 封装到 Set 作为 info.setRoles() 的参数
set.add(role);
//设置该用户拥有的角色
info.setRoles(set);*/
//shiro+jwt方式
Set<String> roleSet = new HashSet<>();
Set<String> permissionSet = new HashSet<>();
//获取用户名
String username = jwtUtil.getUsername(principalCollection.toString());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获得该用户角色
String role = logins.getRole(username);
//每个角色拥有默认的权限
String rolePermission = logins.getPermiss(username);
//每个用户可以设置新的权限
//String permission = logins.getPermission(username);
roleSet.add(role);
permissionSet.add(rolePermission);
info.setRoles(roleSet);
info.setStringPermissions(permissionSet);
return info;
}
}
登录接口
@PostMapping(value = "/login")
@ApiOperation(value = "登录接口",httpMethod = "POST")
@ApiImplicitParams(value = {
@ApiImplicitParam(name = "account",required = true,type = "string"),
@ApiImplicitParam(name = "password",required = true,type = "string")
})
public Result show(@RequestParam(value = "account") String account,@RequestParam(value = "password") String password){
//单shiro
/* // 从SecurityUtils里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(account, password);
// 执行认证登陆
subject.login(token);
//根据权限,指定返回数据
//String role = logins.getRole(account);*/
//shiro+jwt方式
Result result=new Result();
//登录时把生成的token放进redis 失效时间大于token设置失效时间
List list=new LinkedList();
String token=jwtUtil.createToken(account);
//先清空该用户的token
redisUtil.del(account);
tockenImpl.deTocken(account);
//把新的token放进redis
redisUtil.set(account,token,360);
tockenImpl.inTocken(account,token);
login login= logins.show(account,password);
list.add(token);
list.add(login.getAccount());
list.add(login.getId());
result.setList(list);
return result;
}
注销接口
@GetMapping("/logout")
@ApiOperation("注销接口")
public Result logout(@RequestHeader(value = "Token")String token){
Result result=new Result();
String account=jwtUtil.getUsername(token);
redisUtil.del(account);
tockenImpl.deTocken(account);
result.setMessage("注销成功");
return result;
}