大家好,我是程序员田同学。
公司开始了新我的项目,新我的项目的认证采纳的是 Shiro 实现。因为波及到多端登录用户,而且多端用户还是来自不同的表。
这就波及到了 Shiro 的多 realm,明天的 demo 次要是介绍 Shiro 的多 realm 实现计划,文中蕴含所有的代码,须要的敌人能够无缝 copy。
前后端拆散的背景下,在认证的实现中次要是两方面的内容,一个是用户登录获取到 token,二是从申请头中拿到 token 并测验 token 的有效性和设置缓存。
1、用户登录获取 token
登录和以往单 realm 实现逻辑一样,应用用户和明码生成 token 返回给前端,前端每次申请接口的时候携带 token。
@ApiOperation(value="登录", notes="登录")
public Result<JSONObject> wxappLogin(String username,String password){Result<JSONObject> result = new Result<JSONObject>();
JSONObject obj = new JSONObject();
// 生成 token
String password="0";
String token = JwtUtil.sign(username, password);
obj.put("token", token);
result.setResult(obj);
result.success("登录胜利");
return result;
}
生成 token 的工具类
/**
* 生成签名,5min 后过期
*
* @param username 用户名
* @param secret 用户的明码
* @return 加密的 token
*/
public static String sign(String username, String secret) {Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带 username 信息
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
}
以上就实现了简略的登录逻辑,和 Shiro 的单 realm 设置和 SpringSecurity 的登录逻辑都没有什么区别。
2、鉴权登录拦截器(验证 token 有效性)
应用 Shiro 登录拦截器的只须要继承 Shiro 的 BasicHttpAuthenticationFilter 类 重写 isAccessAllowed()办法,在该办法中咱们从 ServletRequest 中获取到 token 和 login_type。
须要特地指出的是,因为是多 realm,咱们在申请头中退出一个 login_type 来辨别不同的登录类型。
通过 token 和 login_type 咱们生成一个 JwtToken 对象提交给 getSubject。
JwtFilter 过滤器
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 默认开启跨域设置(应用单体)*/
private boolean allowOrigin = true;
public JwtFilter(){}
public JwtFilter(boolean allowOrigin){this.allowOrigin = allowOrigin;}
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {executeLogin(request, response);
return true;
} catch (Exception e) {JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
return false;
//throw new AuthenticationException("Token 生效,请从新登录", e);
}
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
String loginType = httpServletRequest.getHeader(CommonConstant.LOGIN_TYPE);
// update-begin--Author:lvdandan Date:20210105 for:JT-355 OA 聊天增加 token 验证,获取 token 参数
if (oConvertUtils.isEmpty(token)) {token = httpServletRequest.getParameter("token");
}
// update-end--Author:lvdandan Date:20210105 for:JT-355 OA 聊天增加 token 验证,获取 token 参数
JwtToken jwtToken = new JwtToken(token,loginType);
// 提交给 realm 进行登入,如果谬误他会抛出异样并被捕捉
getSubject(request, response).login(jwtToken);
// 如果没有抛出异样则代表登入胜利,返回 true
return true;
}
}
JwtToken 类
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
private String token;
private String loginType;
// public JwtToken(String token) {
// this.token = token;
// }
public JwtToken(String token,String loginType) {
this.token = token;
this.loginType=loginType;
}
public String getToken() {return token;}
public void setToken(String token) {this.token = token;}
public String getLoginType() {return loginType;}
public void setLoginType(String loginType) {this.loginType = loginType;}
@Override
public Object getPrincipal() {return token;}
@Override
public Object getCredentials() {return token;}
}
再往下的逻辑必定会先依据咱们的 login_type 来走不同的 realm 了,而后在各自的 realm 中去查看 token 的有效性了,那 Shiro 怎么晓得咱们的 Realm 都是哪些呢?
接下来就该引出应用 Shiro 的外围配置文件了——ShiroConfig.java 类
shiro 的配置文件中会注入名字为 securityManager 的 Bean。
在该 bean 中首先注入 ModularRealmAuthenticator,ModularRealmAuthenticator 会依据配置的 AuthenticationStrategy(身份验证策略)进行多 Realm 认证过程。
因为是多 realm 咱们须要重写 ModularRealmAuthenticator 类,ModularRealmAuthenticator 类中用于判断逻辑走不同的 realm,接着注入咱们的两个 realm,别离是 myRealm 和 clientShiroRealm。
从新注入 ModularRealm 类
@Bean
public ModularRealm ModularRealm(){
// 本人重写的 ModularRealmAuthenticator
ModularRealm modularRealm = new ModularRealm();
// modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());// 这里为默认策略:如果有一个或多个 Realm 验证胜利,所有的尝试都被认为是胜利的,如果没有一个验证胜利,则该次尝试失败
return modularRealm;
}
securityManager-bean。
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm,
ClientShiroRealm clientShiroRealm,ModularRealm modularRealm) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// securityManager.setRealm(myRealm);
securityManager.setAuthenticator(modularRealm);
List<Realm> realms = new ArrayList<>();
// 增加多个 Realm
realms.add(myRealm);
realms.add(clientShiroRealm);
securityManager.setRealms(realms);
/*
* 敞开 shiro 自带的 session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-
* StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
// 自定义缓存实现, 应用 redis
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
ModularRealm 实现类
public class ModularRealm extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {assertRealmsConfigured();
Collection<Realm> realms = getRealms();
// 登录类型对应的所有 Realm
HashMap<String, Realm> realmHashMap = new HashMap<>(realms.size());
for (Realm realm : realms) {
// 这里应用的 realm 中定义的 Name 属性来进行辨别,留神 realm 中要加上
realmHashMap.put(realm.getName(), realm);
}
JwtToken token = (JwtToken) authenticationToken;
if (StrUtil.isEmpty(token.getLoginType())){return doSingleRealmAuthentication(realmHashMap.get(LoginType.DEFAULT.getType()),token);
} else {return doSingleRealmAuthentication(realmHashMap.get(token.getLoginType()),token);
}
// return super.doAuthenticate(authenticationToken);
}
}
而后会依据不同的 login_type 到不同的 realm,上面为我的 Shiro 认证 realm。
myrealm 类.
@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
@Lazy
@Resource
private CommonAPI commonApi;
@Lazy
@Resource
private RedisUtil redisUtil;
@Override
public String getName() {return LoginType.DEFAULT.getType();
}
/**
* 必须重写此办法,不然 Shiro 会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
// return token instanceof JwtToken;
if (token instanceof JwtToken){return StrUtil.isEmpty(((JwtToken) token).getLoginType()) || LoginType.CLIENT.getType().equals(((JwtToken) token).getLoginType());
} else {return false;}
}
/**
* 权限信息认证 (包含角色以及权限) 是用户拜访 controller 的时候才进行验证(redis 存储的此处权限信息)
* 触发检测用户权限时才会调用此办法,例如 checkRole,checkPermission
*
* @param principals 身份信息
* @return AuthorizationInfo 权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {log.debug("===============Shiro 权限认证开始 ============ [ roles、permissions]==========");
String username = null;
if (principals != null) {LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
username = sysUser.getUsername();}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 设置用户领有的角色汇合,比方“admin,test”Set<String> roleSet = commonApi.queryUserRoles(username);
System.out.println(roleSet.toString());
info.setRoles(roleSet);
// 设置用户领有的权限汇合,比方“sys:role:add,sys:user:add”Set<String> permissionSet = commonApi.queryUserAuths(username);
info.addStringPermissions(permissionSet);
System.out.println(permissionSet);
log.info("===============Shiro 权限认证胜利 ==============");
return info;
}
/**
* 用户信息认证是在用户进行登录的时候进行验证(不存 redis)
* 也就是说验证用户输出的账号和明码是否正确,谬误抛出异样
*
* @param auth 用户登录的账号密码信息
* @return 返回封装了用户信息的 AuthenticationInfo 实例
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {log.debug("===============Shiro 身份认证开始 ============doGetAuthenticationInfo==========");
String token = (String) auth.getCredentials();
if (token == null) {HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
log.info("————————身份认证失败——————————IP 地址:"+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
throw new AuthenticationException("token 为空!");
}
// 校验 token 有效性
LoginUser loginUser = null;
try {loginUser = this.checkUserTokenIsEffect(token);
} catch (AuthenticationException e) {JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
e.printStackTrace();
return null;
}
return new SimpleAuthenticationInfo(loginUser, token, getName());
}
/**
* 校验 token 的有效性
*
* @param token
*/
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
// 解密取得 username,用于和数据库进行比照
String username = JwtUtil.getUsername(token);
if (username == null) {throw new AuthenticationException("token 非法有效!");
}
// 查问用户信息
log.debug("———校验 token 是否无效————checkUserTokenIsEffect———————"+ token);
LoginUser loginUser = TokenUtils.getLoginUser(username,commonApi,redisUtil);
//LoginUser loginUser = commonApi.getUserByName(username);
if (loginUser == null) {throw new AuthenticationException("用户不存在!");
}
// 判断用户状态
if (loginUser.getStatus() != 1) {throw new AuthenticationException("账号已被锁定, 请分割管理员!");
}
// 校验 token 是否超时生效 & 或者账号密码是否谬误
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
}
//update-begin-author:taoyan date:20210609 for: 校验用户的 tenant_id 和前端传过来的是否统一
String userTenantIds = loginUser.getRelTenantIds();
if(oConvertUtils.isNotEmpty(userTenantIds)){String contextTenantId = TenantContext.getTenant();
String str ="0";
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
//update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断破绽
String[] arr = userTenantIds.split(",");
if(!oConvertUtils.isIn(contextTenantId, arr)){throw new AuthenticationException("用户租户信息变更, 请从新登陆!");
}
//update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断破绽
}
}
//update-end-author:taoyan date:20210609 for: 校验用户的 tenant_id 和前端传过来的是否统一
return loginUser;
}
/**
* JWTToken 刷新生命周期(实现:用户在线操作不掉线性能)* 1、登录胜利后将用户的 JWT 生成的 Token 作为 k、v 存储到 cache 缓存外面(这时候 k、v 值一样),缓存有效期设置为 Jwt 无效工夫的 2 倍
* 2、当该用户再次申请时,通过 JWTFilter 层层校验之后会进入到 doGetAuthenticationInfo 进行身份验证
* 3、当该用户这次申请 jwt 生成的 token 值曾经超时,但该 token 对应 cache 中的 k 还是存在,则示意该用户始终在操作只是 JWT 的 token 生效了,程序会给 token 对应的 k 映射的 v 值从新生成 JWTToken 并笼罩 v 值,该缓存生命周期从新计算
* 4、当该用户这次申请 jwt 在生成的 token 值曾经超时,并在 cache 中不存在对应的 k,则示意该用户账户闲暇超时,返回用户信息已生效,请从新登录。* 留神:前端申请 Header 中设置 Authorization 放弃不变,校验有效性以缓存中的 token 为准。* 用户过期工夫 = Jwt 无效工夫 * 2。*
* @param userName
* @param passWord
* @return
*/
public boolean jwtTokenRefresh(String token, String userName, String passWord) {String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
if (oConvertUtils.isNotEmpty(cacheToken)) {
// 校验 token 有效性
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
// 生成 token
String newAuthorization = JwtUtil.sign(userName, passWord);
// 设置超时工夫
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
log.debug("——————————用户在线操作,更新 token 保障不掉线—————————jwtTokenRefresh———————"+ token);
}
//update-begin--Author:scott Date:20191005 for:解决每次申请,都重写 redis 中 token 缓存问题
// else {
// // 设置超时工夫
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
// }
//update-end--Author:scott Date:20191005 for:解决每次申请,都重写 redis 中 token 缓存问题
return true;
}
//redis 中不存在此 TOEKN,阐明 token 非法返回 false
return false;
}
/**
* 革除以后用户的权限认证缓存
*
* @param principals 权限信息
*/
@Override
public void clearCache(PrincipalCollection principals) {super.clearCache(principals);
}
}
ClientShiroRealm 类.
@Component
@Slf4j
public class ClientShiroRealm extends AuthorizingRealm {
@Lazy
@Resource
private ClientAPI clientAPI;
@Lazy
@Resource
private RedisUtil redisUtil;
@Override
public String getName() {return LoginType.CLIENT.getType();
}
/**
* 必须重写此办法,不然 Shiro 会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
// return token instanceof JwtToken;
if (token instanceof JwtToken){return LoginType.CLIENT.getType().equals(((JwtToken) token).getLoginType());
} else {return false;}
}
/**
* 权限信息认证 (包含角色以及权限) 是用户拜访 controller 的时候才进行验证(redis 存储的此处权限信息)
* 触发检测用户权限时才会调用此办法,例如 checkRole,checkPermission
*
* @param principals 身份信息
* @return AuthorizationInfo 权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {log.debug("===============Shiro 权限认证开始 ============ [ roles、permissions]==========");
//String username = null;
//if (principals != null) {// LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
// username = sysUser.getUsername();
//}
//SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//// 设置用户领有的角色汇合,比方“admin,test”//Set<String> roleSet = commonApi.queryUserRoles(username);
//System.out.println(roleSet.toString());
//info.setRoles(roleSet);
//
//// 设置用户领有的权限汇合,比方“sys:role:add,sys:user:add”//Set<String> permissionSet = commonApi.queryUserAuths(username);
//info.addStringPermissions(permissionSet);
//System.out.println(permissionSet);
log.info("===============Shiro 权限认证胜利 ==============");
return null;
}
/**
* 用户信息认证是在用户进行登录的时候进行验证(不存 redis)
* 也就是说验证用户输出的账号和明码是否正确,谬误抛出异样
*
* @param auth 用户登录的账号密码信息
* @return 返回封装了用户信息的 AuthenticationInfo 实例
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {log.debug("===============Shiro 身份认证开始 ============doGetAuthenticationInfo==========");
String token = (String) auth.getCredentials();
if (token == null) {HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
log.info("————————身份认证失败——————————IP 地址:"+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
throw new AuthenticationException("token 为空!");
}
// 校验 token 有效性
LoginUser loginUser = null;
try {loginUser = this.checkUserTokenIsEffect(token);
} catch (AuthenticationException e) {JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
e.printStackTrace();
return null;
}
return new SimpleAuthenticationInfo(loginUser, token, getName());
}
/**
* 校验 token 的有效性
*
* @param token
*/
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
// 解密取得 username,用于和数据库进行比照
String username = JwtUtil.getUsername(token);
if (username == null) {throw new AuthenticationException("token 非法有效!");
}
// 查问用户信息
log.debug("———校验 token 是否无效————checkUserTokenIsEffect———————"+ token);
LoginUser loginUser = TokenUtils.getClientLoginUser(username,clientAPI,redisUtil);
//LoginUser loginUser = commonApi.getUserByName(username);
if (loginUser == null) {throw new AuthenticationException("用户不存在!");
}
// 校验 token 是否超时生效 & 或者账号密码是否谬误
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
}
return loginUser;
}
/**
* JWTToken 刷新生命周期(实现:用户在线操作不掉线性能)* 1、登录胜利后将用户的 JWT 生成的 Token 作为 k、v 存储到 cache 缓存外面(这时候 k、v 值一样),缓存有效期设置为 Jwt 无效工夫的 2 倍
* 2、当该用户再次申请时,通过 JWTFilter 层层校验之后会进入到 doGetAuthenticationInfo 进行身份验证
* 3、当该用户这次申请 jwt 生成的 token 值曾经超时,但该 token 对应 cache 中的 k 还是存在,则示意该用户始终在操作只是 JWT 的 token 生效了,程序会给 token 对应的 k 映射的 v 值从新生成 JWTToken 并笼罩 v 值,该缓存生命周期从新计算
* 4、当该用户这次申请 jwt 在生成的 token 值曾经超时,并在 cache 中不存在对应的 k,则示意该用户账户闲暇超时,返回用户信息已生效,请从新登录。* 留神:前端申请 Header 中设置 Authorization 放弃不变,校验有效性以缓存中的 token 为准。* 用户过期工夫 = Jwt 无效工夫 * 2。*
* @param userName
* @param passWord
* @return
*/
public boolean jwtTokenRefresh(String token, String userName, String passWord) {String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
if (oConvertUtils.isNotEmpty(cacheToken)) {
// 校验 token 有效性
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
// 生成 token
String newAuthorization = JwtUtil.sign(userName, passWord);
// 设置超时工夫
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
log.debug("——————————用户在线操作,更新 token 保障不掉线—————————jwtTokenRefresh———————"+ token);
}
return true;
}
//redis 中不存在此 TOEKN,阐明 token 非法返回 false
return false;
}
/**
* 革除以后用户的权限认证缓存
*
* @param principals 权限信息
*/
@Override
public void clearCache(PrincipalCollection principals) {super.clearCache(principals);
}
}
这两个 realm 更多的是须要实现咱们本身的 realm,我把我的全副代码贴上,读者可依据本人的须要进行批改,两个办法大抵的作用都是测验 token 的有效性,只是查问的用户从不同的用户表中查出来的。
至此,Shiro 的多 Realm 实现计划到这里就正式完结了。