spring boot+shiro+jwt结合微信实现免密登录+权限控制
本人第一次写博客,如有错误请大佬们指正
情况说明
由于需求原因,在微信授权登录的基础上需要实现免密登录,在微信第三方页面中集成了后台管理,此处需要用权限做验证,思来想去,用拦截器也可以实现,但由于强迫症的原因,还是想用shiro做权限验证,此处将项目中遇到的问题在此说出,不正确的地方还请各位指正!
一.导入maven
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
二.免密登录
1.在项目中继承了shiro,但都是基于账号密码作出的登录认证和授权,这与我项目需求不符合,此处贴出结合shiro免密登录 大概意思:继承 shiro 的 UsernamePasswordToken ,给定特定的免密标识。
import org.apache.shiro.authc.UsernamePasswordToken;
/**
* 重写UsernamePasswordToken 免密登录调用方法和密码登录
* @ClassName: UsernamePasswordToken
* @author
* @date 2021年05月11日
*/
public class CustomToken extends UsernamePasswordToken {
private static final long serialVersionUID = -2564928913725078138L;
private LoginType type;
public CustomToken() {
super();
}
public CustomToken(String username, String password, LoginType type, boolean rememberMe, String host) {
super(username, password, rememberMe, host);
this.type = type;
}
public LoginType getType() {
return type;
}
public void setType(LoginType type) {
this.type = type;
}
/**
* 免密登录
* @param username
*/
public CustomToken(String username) {
super(username, "", false, null);
this.type = LoginType.NOPASSWD;
}
/**
* 账号密码登录
* @param username
* @param pwd
*/
public CustomToken(String username, String pwd) {
super(username, pwd, false, null);
this.type = LoginType.PASSWORD;
}
}
2.重写 HashedCredentialsMatcher,不需要密码登录 用与shiro免密识别免密登录标识,并在 shiroconfig 配置文件中配置
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.springframework.context.annotation.Configuration;
/**
* 重写HashedCredentialsMatcher,不需要密码登录 用与shiro免密登录
* @ClassName: HashedCredentialsMatcher
* @author jiangb
* @date 2021年05月13日
*/
@Configuration
public class MyRetryLimitCredentialsMatcher extends HashedCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info) {
//获取自定义继承了UsernamePasswordToken的用户密码token类
CustomToken tk = (CustomToken) authcToken;
//将类中的登录标志符拿出来比对,如果是免密登录,则返回ture
if(tk.getType().equals(LoginType.NOPASSWD)){
return true;
}
boolean matches = super.doCredentialsMatch(authcToken, info);
return matches;
}
}
3.登录类型枚举
public enum LoginType {
PASSWORD("password"), // 密码登录
NOPASSWD("nopassword"); // 免密登录
private String code;// 状态值
private LoginType(String code) {
this.code = code;
}
public String getCode () {
return code;
}
}
4.自己配置的MyRealm
import lombok.extern.slf4j.Slf4j;
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.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.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
@Component
@Slf4j
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private PermissionMapper permissionMapper;
/**
* 表示根据用户身份获取授权信息。授权认证
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("----------------开始授权认证-----------");
UserVo user = (UserVo)principalCollection.getPrimaryPrincipal();
//通过用户名查询该用户的所有权限
List<Permission> permissionList = permissionMapper.findPermissionByUserName(user.getUsername());
//使用stream流循环去重添加到set中
HashSet<String> collect = (HashSet<String>) permissionList.stream().map(a -> a.getPermissionName()).collect(Collectors.toSet());
//声明 将权限设置到该对象中
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setStringPermissions(collect);
//返回授权信息
log.info("----------------授权认证结束-----------");
return simpleAuthorizationInfo;
}
/**
* 表示登录获取身份验证信息; 登录认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("----------------开始登陆认证-----------");
//获取用户名信息
String userName = (String)authenticationToken.getPrincipal();
//根据用户名去数据库查询该用户
UserVo userVo = userService.findByUserName(userName);
//声明SimpleAuthenticationInfo 将前端传入的用户名,以及从数据库查询楚的密码
//以及当前类的名称放置生成SimpleAuthenticationInfo 进行返回
SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo(userVo,userVo.getUsername(),this.getName());
log.info("----------------登陆认证结束-----------");
return simpleAuthenticationInfo;
}
/**
* 清除当前用户的权限认证缓存
*
* @param principals 权限信息
*/
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
}
三.shiro配置文件
重点在这,当时我实现了微信授权登录+shiro的免密登录,数据也入库了,但是就是验证权限的时候死活不进MyRealm的权限认证。还是因为自己的粗心大意,重点必须在SecurityManager
bean中配置自己自定义的shiro session 缓存管理器,否则,前后端分离时,拿不到sessionID,识别不了用户,就会出现已经登录了,还是识别不了登陆用户的错误。
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
@Configuration
@Slf4j
public class ShiroConfig {
//shiro的过滤器
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean sf = new ShiroFilterFactoryBean();
sf.setSecurityManager(securityManager);
LinkedHashMap<String, String> filterChainDe = new LinkedHashMap<>();
//配置你自己需要权限的拦截接口
filterChainDe.put("/**/**", "authc");
//配置自己的放行接口
filterChainDe.put("/login", "anon");
filterChainDe.put("/callBack", "anon");
sf.setFilterChainDefinitionMap(filterChainDe);
return sf;
}
@Bean
public MyRealm myRealm() {
MyRealm myRealm=new MyRealm();
myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myRealm;
}
// 必须使用session管理器,才能够解决前后端分离shiro的subject未认证的问题
@Bean
public SessionManager sessionManager(){
//将我们继承后重写的shiro session 注册
ShiroSession shiroSession = new ShiroSession();
shiroSession.setSessionDAO(new EnterpriseCacheSessionDAO());
return shiroSession;
}
// 它继承了DefaultSecurityManager类,实现了WebSecurityManager接口,没有使用这个类,无法验证会话中登录的用户,也无法进行登录校验 登出
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//defaultWebSecurityManager.setCacheManager(cacheManager());
defaultWebSecurityManager.setRealm(myRealm());
//自定义的shiro session 缓存管理器,
//此处需特别注意,需要set自定义的session管理器,否则会出现已登录,但找不到登录用户信息的错误
defaultWebSecurityManager.setSessionManager(sessionManager());
return defaultWebSecurityManager;
}
//开启shiro权限注解模式
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor=new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
//使用自定义的免密登录方式
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
MyRetryLimitCredentialsMatcher matcher = new MyRetryLimitCredentialsMatcher();
return matcher;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
四.前后端分离实现登录认证及权限认证
shiro配置文件配完后,需要在登录时,主动获取sessionId,并返回给前端,让前端保存到 localstorage 中
1.登录
Subject subject = SecurityUtils.getSubject();
String sessionId = (String) subject.getSession().getId();
log.info("获取sessionId "+sessionId);
//使用自定义的免密登录方式
CustomToken token =new CustomToken(user.getUsername());
log.info("开始免密登录");
subject.login(token);
if (subject.isAuthenticated()) {
//自己的登录逻辑,并把sessionId 返回给前端
}
2.返回给前端,让前端保存到 localstorage 中
localStorage.setItem("authToken", res.data.authToken)
3.在自定的shiroSession管理中 ,通过获取请求头的方式拿到sessionId,在使用注解实现权限验证的时候(我用的是@RequiresPermissions),就会根据sessionId 拿到登录认证信息,进入到权限认证中,实现权限的校验(代码注解说明)
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* 目的: shiro 的 session 管理
* 自定义session规则,实现前后分离,在跨域等情况下使用token 方式进行登录验证才需要,否则没必须使用本类。
* 在前后端分离的时候,由于跨域和其他的原因sessionID会失效,或者出现
* 当时登录后拿到的登录认证信息不是现在登录的认证信息,相当于登录了
* 两次,系统会识别新一次的认证信息,这时候再进入到权限认证中,就会
* 报信息不匹配的错误,这里起到了一个维护会话的作用,相当于浏览器的cookie
*
*/
@Slf4j
public class ShiroSession extends DefaultWebSessionManager {
/**
* 定义的请求头中使用的标记key,用来传递 token
*/
private static final String AUTH_TOKEN = "authToken";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public ShiroSession() {
super();
//设置 shiro session 失效时间,默认为30分钟,这里现在设置为15分钟
setGlobalSessionTimeout(MILLIS_PER_MINUTE * 60);
}
/**
* 获取sessionId,原本是根据sessionKey来获取一个sessionId
* @param request ServletRequest
* @param response ServletResponse
* @return Serializable
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//获取请求头中的 AUTH_TOKEN 的值,如果请求头中有 AUTH_TOKEN 则其值为sessionId。shiro就是通过sessionId 来控制的
log.info("获取authToken");
String sessionId = WebUtils.toHttp(request).getHeader(AUTH_TOKEN);
if (StringUtils.isEmpty(sessionId)){
//如果没有携带id参数则按照父类的方式在cookie进行获取sessionId
return super.getSessionId(request, response);
} else {
//请求头中如果有 authToken, 则其值为sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
//sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
}
}
}
此篇文章用于记录自己项目中所遇到的错误,如有不足之处,还望进行指点!
本文借鉴了这篇博客: https://blog.csdn.net/qq_43114230/article/details/112490186
如有侵权,请联系删除!