共计 15305 个字符,预计需要花费 39 分钟才能阅读完成。
大家好,我是程序员田同学。
公司开始了新我的项目,新我的项目的认证采纳的是 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 实现计划到这里就正式完结了。