shiro中是通过realm进行权限认证的,本身shiro是支持多realm进行权限认证的。

现有场景如下。

平台分管理角色和互联网角色,分别存储在sys_user和student表中。sys_user当然是通过管理后台来进行创建。student表中的用户是允许前端注册的,不走后台权限角色体系。

但是提供的接口均需要实现token的安全验证,毕竟不能裸奔不是。所以需要在基础框架基础上,实现多realm认证,并且使用相同token。主要是不想拆分两个后台api项目。

首先项目pom中需要导入相应的包。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>

这里根据需要自己导。

因为需要token区分认证用户,所以JWTToken需要增加个标志

private String loginType;

public JWTToken(String token,String loginType) {

this.token = token;
this.loginType=loginType;
}

public String getLoginType() {

return loginType;
}

public void setLoginType(String loginType) {

this.loginType = loginType;
}

createToken的时候需要根据loginType进行下处理

public static String createAccessToken(String username, String password,String loginType) {
Date date = new Date(System.currentTimeMillis() + JeePlusProperites.newInstance().getEXPIRE_TIME());
Algorithm algorithm = Algorithm.HMAC256(password);
// 附带username信息
return JWT.create()
.withClaim("username", username+"_"+loginType)
.withExpiresAt(date)
.sign(algorithm);
}

public static String createRefreshToken(String username, String password,String loginType) {
Date date = new Date(System.currentTimeMillis() + 3*JeePlusProperites.newInstance().getEXPIRE_TIME());
Algorithm algorithm = Algorithm.HMAC256(password);
// 附带username信息
return JWT.create()
.withClaim("username", username+"_"+loginType)
.withExpiresAt(date)
.sign(algorithm);
}

public static String getLoginName(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
String userName = jwt.getClaim("username").asString();
if(userName.contains("_")){
userName = userName.substring(0,userName.lastIndexOf("_"));
}

return userName;
} catch (JWTDecodeException e) {
return null;
}
}

public static int verify(String token,String loginType) {
try {
String loginName = JWTUtil.getLoginName(token);
String userName = JWTUtil.getUserName(token);
String password = "";
if(loginType.equals(UserConstant.SYS)){
// UserUtils里面使用loginName进行的redis缓存,这里不能加logintype后缀
password = UserUtils.getByLoginName(loginName).getPassword();
}
else if(loginType.equals(UserConstant.STUDENT)){
password = UserUtils.getStudentByPhone(loginName).getPassword();
}

Algorithm algorithm = Algorithm.HMAC256(password);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", userName)
.build();
DecodedJWT jwt = verifier.verify(token);
return 0;
} catch (TokenExpiredException e){
return 1;
}
catch (Exception exception) {
return 2;
}
}

主要是缓存的username中进行了扩展,这里通过"_"+loginType进行了扩展。

相应的如果做了redis缓存要注意key值用的那个。

同时处理了刷新token和验证token的方法。具体参考上述代码

shiro是支持多realm的,但是大多数框架中,都是只用的default的默认的realm进行认证。想要启用多realm支持,需要自己重写ModularRealmAuthenticator实现。

public class ModularRealm extends ModularRealmAuthenticator {

@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
// 判断getRealms()是否返回为空
assertRealmsConfigured();
// 所有Realm
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;
// 登录类型
String type = token.getLoginType();
if (realmHashMap.get(type) != null) {
return doSingleRealmAuthentication(realmHashMap.get(type), token);
} else {
return doMultiRealmAuthentication(realms, token);
}
}
}

然后就是是先具体的realm了

package com.jeeplus.modules.sys.security;

import com.jeeplus.common.utils.CacheUtils;
import com.jeeplus.modules.sys.entity.StudentUser;
import com.jeeplus.modules.sys.entity.User;
import com.jeeplus.modules.sys.security.shiro.JWTToken;
import com.jeeplus.modules.sys.security.util.JWTUtil;
import com.jeeplus.modules.sys.service.StudentUserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

public class StudentRealm extends AuthorizingRealm {
@Autowired
private StudentUserService stuService;
// @Autowired
// private RedisUtil redisUtil;

/**
* 使用JWT替代原生Token
*
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}

private static final String ADMIN_LOGIN_TYPE = UserConstant.STUDENT;
{
super.setName("student"); //设置realm的名字,和上面管理器对应起来
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
String token = (String) authcToken.getCredentials();
// 解密获得username,用于和数据库进行对比
String loginName = JWTUtil.getLoginName(token);
if (loginName == null) {
throw new AuthenticationException("无效的token");
}
StudentUser cachedUser = (StudentUser) CacheUtils.get("LoginStudent", loginName);
if(cachedUser == null){
throw new AuthenticationException("用户信息已过期,请重新登陆.");
}
// 使用手机号进行用户判定
StudentUser user = stuService.getStudentByPhone(loginName);
if(user == null){
throw new AuthenticationException("用户不存在.");
}

int rtn = JWTUtil.verify(token, UserConstant.STUDENT);
if (rtn == 1) {
throw new AuthenticationException("token已经过期");
}else if (rtn == 2) {
throw new AuthenticationException("用户名或者密码错误");
}

return new SimpleAuthenticationInfo(token, token, getName());
}
}
/**
* Copyright &copy; 2015-2020 <a href="http://www.jeeplus.org/">JeePlus</a> All rights reserved.
*/
package com.jeeplus.modules.sys.security;

import com.jeeplus.common.utils.CacheUtils;
import com.jeeplus.common.utils.SpringContextHolder;
import com.jeeplus.config.properties.JeePlusProperites;
import com.jeeplus.config.web.Servlets;
import com.jeeplus.modules.sys.entity.LogType;
import com.jeeplus.modules.sys.entity.Menu;
import com.jeeplus.modules.sys.entity.Role;
import com.jeeplus.modules.sys.entity.User;
import com.jeeplus.modules.sys.security.shiro.JWTToken;
import com.jeeplus.modules.sys.security.util.JWTUtil;
import com.jeeplus.modules.sys.service.UserService;
import com.jeeplus.modules.sys.utils.LogUtils;
import com.jeeplus.modules.sys.utils.UserUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.List;

/**
* 系统安全认证实现类
* @author jeeplus
* @version 2017-7-5
*/
@Service
public class SystemAuthorizingRealm extends AuthorizingRealm {

private Logger logger = LoggerFactory.getLogger(getClass());

private UserService userService;

@Autowired
private JeePlusProperites jeePlusProperites;

/**
* 大坑!,必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}

/**
* 认证回调函数, 登录时调用
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {

String token = (String) authcToken.getCredentials();
// 解密获得username,用于和数据库进行对比
String loginName = JWTUtil.getLoginName(token);
if (loginName == null) {
throw new AuthenticationException("无效的token");
}
User cachedUser = (User) CacheUtils.get("LoginUser", loginName);
if(cachedUser == null){
throw new AuthenticationException("用户信息已过期,请重新登陆.");
}
// 校验用户
User user = getUserService().getUserByLoginName(loginName);
if(user == null){
throw new AuthenticationException("用户不存在.");
}

int rtn = JWTUtil.verify(token, UserConstant.SYS);
if (rtn == 1) {
throw new AuthenticationException("token已经过期");
}else if (rtn == 2) {
throw new AuthenticationException("用户名或者密码错误");
}

// 校验用户名密码
if (JeePlusProperites.NO.equals(user.getLoginFlag())){
throw new AuthenticationException("该已帐号禁止登录.");
}
return new SimpleAuthenticationInfo(token, token, getName());
}

/**
* 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

String username = JWTUtil.getLoginName(principals.toString());
// 获取当前已登录的用户
User user = getUserService().getUserByLoginName(username);
if (user != null) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<Menu> list = UserUtils.getMenuList();
for (Menu menu : list){
if (StringUtils.isNotBlank(menu.getPermission())){
// 添加基于Permission的权限信息
for (String permission : StringUtils.split(menu.getPermission(),",")){
info.addStringPermission(permission);
}
}
}
// 添加用户权限
info.addStringPermission("user");
// 添加用户角色信息
for (Role role : user.getRoleList()){
info.addRole(role.getEnname());
}
// 更新登录IP和时间
getUserService().updateUserLoginInfo(user);
// 记录登录日志
LogUtils.saveLog(Servlets.getRequest(), "系统登录", LogType.LOGIN.getType());
return info;
} else {
return null;
}
}



}

代码不全,仅供参考

接下来配置过滤器

package com.jeeplus.modules.sys.security.shiro;

import com.jeeplus.common.utils.CookieUtils;
import com.jeeplus.common.utils.StringUtils;
import com.jeeplus.config.properties.JeePlusProperites;
import com.jeeplus.core.web.GlobalErrorController;
import com.jeeplus.modules.sys.security.util.JWTUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class JWTFilter extends BasicHttpAuthenticationFilter {

private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

@Autowired
private JeePlusProperites jeePlusProperites;

/**
* 判断用户是否想要登入。
* 检测header里面是否包含Token字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = getToken(req);
return authorization != null && !"null".equals(authorization)&& !"".equals(authorization);
}

/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = getToken(httpServletRequest);
String loginType=httpServletRequest.getHeader("loginType");
JWTToken token = new JWTToken(authorization,loginType);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}

/**
* 获取token,支持三种方式, 请求参数、header、cookie, 优先级依次降低,以请求参数中的优先级最高。
* @param httpServletRequest
* @return
*/
private String getToken(HttpServletRequest httpServletRequest){
String token0 = httpServletRequest.getParameter(JWTUtil.TOKEN);
String token1 = httpServletRequest.getHeader(JWTUtil.TOKEN);
String token2 = CookieUtils.getCookie(httpServletRequest, JWTUtil.TOKEN);
if(StringUtils.isNotBlank(token0)){
return token0;
}
if(StringUtils.isNotBlank(token1)){
return token1;
}
if(StringUtils.isNotBlank(token2)){
return token2;
}
return null;
}


@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
try {
return executeLogin(request, response);
} catch (AuthenticationException e) {
GlobalErrorController.response401(request, response);//登录超时,需要刷新token
}catch (Exception e){
GlobalErrorController.response4021(request, response);//没有登录,需要登录
}
}else {
GlobalErrorController.response4021(request, response);//没有登录,需要登录
}

return false;

}

/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}


}

然后是shiroconfig

@Bean
public SystemAuthorizingRealm SystemAuthorizingRealm() {
SystemAuthorizingRealm backRealm = new SystemAuthorizingRealm();
backRealm.setName(UserConstant.SYS);
return backRealm;
}

@Bean
public StudentRealm StudentRealm() {
StudentRealm backRealm = new StudentRealm();
backRealm.setName(UserConstant.STUDENT);
return backRealm;
}

@Bean
public Authenticator authenticator() {
ModularRealm modularRealm = new ModularRealm();
modularRealm.setRealms(Arrays.asList(SystemAuthorizingRealm(), StudentRealm()));
modularRealm.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return modularRealm;
}

@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(
SystemAuthorizingRealm systemAuthorizingRealm,
WxUserRealm wxUserRealm,
SessionManager sessionManager,
CacheManager cacheManager) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

defaultWebSecurityManager.setSessionManager(sessionManager);
defaultWebSecurityManager.setCacheManager(cacheManager);

//多realm
Set<Realm> realms = new HashSet<Realm>();
realms.add(SystemAuthorizingRealm());
realms.add(StudentRealm());
defaultWebSecurityManager.setRealms(realms);

defaultWebSecurityManager.setAuthenticator(authenticator());//注意这里要配置上
return defaultWebSecurityManager;
}

然后后台Login方法

/**
* PC登录
* @param userName
* @param password
* @return
* @throws Exception
*/
@PostMapping("/sys/login")
@ApiOperation("登录接口")
public AjaxJson login(@RequestParam("userName") String userName,
@RequestParam("password") String password
) throws Exception {
AjaxJson j = new AjaxJson();
User user = UserUtils.getByLoginName(userName);
// if (user != null && UserService.validatePassword(password, user.getPassword())) {
if (user != null && MD5.md5validate(password, user.getPassword())) {

if (JeePlusProperites.NO.equals(user.getLoginFlag())){
j.setSuccess(false);
j.setMsg("该用户已经被禁止登陆!");
}else {
CacheUtils.put("LoginUser", userName, user);
j.setSuccess(true);
j.put(JWTUtil.TOKEN, JWTUtil.createAccessToken(userName, user.getPassword(), UserConstant.SYS));
j.put(JWTUtil.REFRESH_TOKEN, JWTUtil.createRefreshToken(userName, user.getPassword(), UserConstant.SYS));
}

} else {
j.setSuccess(false);
j.setMsg("用户名或者密码错误!");
}
j.put("pwdIsOk", PasswordUtils.pwdIsOk(password));
return j;
}

/**
* 学生登陆
* @param phone
* @param password
* @return
* @throws Exception
*/
@PostMapping("/student/login")
@ApiOperation("登录接口")
public AjaxJson studentLogin(@RequestParam("phone") String phone,
@RequestParam("password") String password
) throws Exception {
AjaxJson j = new AjaxJson();
StudentUser user = UserUtils.getStudentByPhone(phone);
// if (user != null && UserService.validatePassword(password, user.getPassword())) {
if (user != null && passwordEncoder.matches(password, user.getPassword())) {

CacheUtils.put("LoginStudent", phone, user);
j.setSuccess(true);
j.put(JWTUtil.TOKEN, JWTUtil.createAccessToken(phone, user.getPassword(), UserConstant.STUDENT));
j.put(JWTUtil.REFRESH_TOKEN, JWTUtil.createRefreshToken(phone, user.getPassword(),UserConstant.STUDENT));

} else {
j.setSuccess(false);
j.setMsg("用户名或者密码错误!");
}
j.put("pwdIsOk", PasswordUtils.pwdIsOk(password));
return j;
}
/**
* 学生退出登录
* @throws IOException
*/
@ApiOperation("学生用户退出")
@GetMapping("/student/logout")
public AjaxJson studentLogout() {
AjaxJson j = new AjaxJson();
String token = UserUtils.getToken();
if (StringUtils.isNotBlank(token)) {
UserUtils.clearStudentCache();
UserUtils.getSubject().logout();
}
j.setMsg("退出成功");
return j;
}

@GetMapping("/student/refreshToken")
@ApiOperation("刷新token")
public AjaxJson studentAccessTokenRefresh(String refreshToken, HttpServletRequest request, HttpServletResponse response){

int rtn = JWTUtil.verify(refreshToken,UserConstant.STUDENT);
if (rtn == 1) {
GlobalErrorController.response4022(request, response);
}else if (rtn == 2) {
return AjaxJson.error("用户名密码错误");
}

// String loginName = JWTUtil.getLoginName(refreshToken);
String loginName = JWTUtil.getLoginName(refreshToken);
String userName = JWTUtil.getUserName(refreshToken);

String password = UserUtils.getStudentByPhone(loginName).getPassword();
//创建新的accessToken
String accessToken = JWTUtil.createAccessToken(userName, password,UserConstant.STUDENT);

//下面判断是否刷新 REFRESH_TOKEN,如果refreshToken 快过期了 需要重新生成一个替换掉
long minTimeOfRefreshToken = 2*JeePlusProperites.newInstance().getEXPIRE_TIME();//REFRESH_TOKEN 有效时长是应该为accessToken有效时长的2倍
Long refreshTokenExpirationTime = JWT.decode(refreshToken).getExpiresAt().getTime();//refreshToken创建的起始时间点
//(refreshToken过期时间- 当前时间点) 表示refreshToken还剩余的有效时长,如果小于2倍accessToken时长 ,则刷新 REFRESH_TOKEN
if(refreshTokenExpirationTime - System.currentTimeMillis() <= minTimeOfRefreshToken){
//刷新refreshToken
refreshToken = JWTUtil.createRefreshToken(userName, password,UserConstant.STUDENT);
}

return AjaxJson.success().put(JWTUtil.TOKEN, accessToken).put(JWTUtil.REFRESH_TOKEN, refreshToken);
}

前端请求

// 请求头带上token
config.headers.token = Vue.cookie.get('token')
config.headers.loginType = "sys"

请求头需要token和loginType两部分

if (token) {
header.token = token; // 获取token值
header.loginType = "student"
}

一般都会有同一的request.js进行处理,自己对着调整

到这里基本就可以了,想要了解完整流程的,可以自己debug跟一遍。

总的来说,是请求过去,先被拦截器拦截,然后根据请求头的logintype,分别走不通的realm进行认证和验证。不同的realm中,可以走不通的表进行验证。这样就可以一套jwt系统同时支持后台系统用户,移动端用户,web端用户了。

代码不全,领会精神