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);
}
}
}
代码不全,仅供参考
接下来配置过滤器
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"
}
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://www.95app.top/shiro-%e5%a4%9a%e7%94%a8%e6%88%b7%e4%bd%93%e7%b3%bb%e7%99%bb%e5%bd%95/