记录一下使用SpringBoot集成Shiro框架实现前后端分离Web项目的过程,后端使用SpringBoot整合Shiro,前端使用vue+elementUI,达到前后端使用token来进行交互的应用,这种方式通常叫做无状态,后端只需要使用Shiro框架根据前端传来的token信息授权访问相应资源。
案例源码:
SpringBoot+Shiro框架整合实现前后端分离的权限管理基础Demo
首先新建SpringBoot项目,导入Springboot整合shiro所需要的依赖包
<!-- SpringBoot整合shiro所需相关依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.10.0</version>
</dependency>
<!--web模块的启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
使用的SpringBoot版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
使用SpringBoot集合Shiro之前,需要建立相应的类和从数据库获取的用户数据(这里新建一个java静态类来模拟解决)
用户登录的类
UserValidate.java
package boot.example.shiro.domain;
/**
* 蚂蚁舞
*/
public class UserValidate {
String username;
String password;
// get set
}
用户类SysUsers.java
package boot.example.shiro.domain;
/**
* 蚂蚁舞
*/
public class SysUsers {
private Integer user_id;
private String username;
private String password;
private int user_type; // 用户类型 -1表示超级账号 1表示普通账号
private Integer role_id; // 用户角色 拿权限需要的
private Integer locked; // 用户状态 1-正常 2=锁定
public SysUsers() {
}
public SysUsers(Integer user_id, String username, String password, int user_type, Integer role_id, Integer locked) {
this.user_id = user_id;
this.username = username;
this.password = password;
this.user_type = user_type;
this.role_id = role_id;
this.locked = locked;
}
// get set
}
模拟三个用户shiro_admin, myw_admin, app_admin以及相关的方法和静态mock数据
模拟数据库的类
ShiroDataMapper.java
package boot.example.shiro.config;
import boot.example.shiro.domain.SysUsers;
import java.util.ArrayList;
import java.util.List;
/**
* 蚂蚁舞
*/
public class ShiroDataMapper {
private static final String shiro_admin = "shiro_admin";
private static final String myw_admin = "myw_admin";
private static final String app_admin = "app_admin";
private static final SysUsers sysUsers_shiro_admin = new SysUsers(1, shiro_admin, "123", -1, 1, 1);
private static final SysUsers sysUsers_myw_admin = new SysUsers(2, myw_admin, "1234", 1, 2, 1);
private static final SysUsers sysUsers_app_admin = new SysUsers(3, app_admin, "12345",3, 3, 1);
public static SysUsers getSysUsersByUserName(String username){
if(username.equalsIgnoreCase(shiro_admin)){
return sysUsers_shiro_admin;
}
if(username.equalsIgnoreCase(myw_admin)){
return sysUsers_myw_admin;
}
if(username.equalsIgnoreCase(app_admin)){
return sysUsers_app_admin;
}
return null;
}
public static List<String> listSysRolesPermissions(Integer roleId){
if(roleId == 2){
List<String> list = new ArrayList<>();
list.add("sys:user:list");
list.add("sys:user:update");
list.add("sys:user:add");
list.add("sys:user:delete");
return list;
}
if(roleId == 3){
List<String> list = new ArrayList<>();
list.add("sys:user:list");
return list;
}
return null;
}
}
getSysUsersByUserName方法是用来模拟从数据库获取用户对象数据的,listSysRolesPermissions是根据用户的角色来获取对应的权限列表的。
Shiro框架的ShiroRealm.java
shiro的realm主要用来实现认证(
AuthenticationInfo
)和授权(
AuthorizationInfo
)
package boot.example.shiro.config;
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// to do
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// to do
}
}
认证的实现,当用户通过接口登录后就会触发这里的认证登录
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取登录username
String username = (String)token.getPrincipal();
// 从数据库获取用户对象 (这里模拟的)
SysUsers sysUsers = ShiroDataMapper.getSysUsersByUserName(username);
// 在数据库里没找到用户,异常用户,抛出异常(交给异常处理)
if(sysUsers == null) {
throw new UnknownAccountException(); //没找到帐号
}
// 一般用户允不允许登录也是有一个锁定状态的 从用户对象里拿到锁定状态,判断是否锁定
if(2 == sysUsers.getLocked()) {
throw new LockedAccountException(); //帐号锁定
}
// 交给SimpleAuthenticationInfo去验证密码
return new SimpleAuthenticationInfo(sysUsers, sysUsers.getPassword(), this.getClass().getName());
}
授权实现,给超级管理所有权限,给具体的普通用户对应的权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取用户对象
SysUsers user = (SysUsers)principals.getPrimaryPrincipal();
// 对象为null 抛出异常
if(user == null){
throw new UnknownAccountException();
}
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUsers sysUsers = ShiroDataMapper.getSysUsersByUserName(user.getUsername());
if(sysUsers == null){
throw new UnknownAccountException();
}
// // 用户类型 -1表示超级账号 1表示普通账号
if(sysUsers.getUser_type() < 0){
authorizationInfo.addRole("*"); // roles的权限 所有
authorizationInfo.addStringPermission("*:*:*"); // perms的权限 所有
} else {
// 用角色id从数据库获取权限列表,这里是模拟的
List<String> mapList = ShiroDataMapper.listSysRolesPermissions(sysUsers.getRole_id());
authorizationInfo.addRole("key");
if (!mapList.isEmpty()) {
Set<String> permsSet = new HashSet<>();
for (String perm : mapList) {
permsSet.addAll(Arrays.asList(perm.trim().split(",")));
}
authorizationInfo.setStringPermissions(permsSet);
}
}
return authorizationInfo;
}
ShiroRealm.java完整代码
package boot.example.shiro.config;
import boot.example.shiro.domain.SysUsers;
import org.apache.shiro.authc.*;
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.stereotype.Service;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 蚂蚁舞
*/
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取用户对象
SysUsers user = (SysUsers)principals.getPrimaryPrincipal();
// 对象为null 抛出异常
if(user == null){
throw new UnknownAccountException();
}
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUsers sysUsers = ShiroDataMapper.getSysUsersByUserName(user.getUsername());
if(sysUsers == null){
throw new UnknownAccountException();
}
// // 用户类型 -1表示超级账号 1表示普通账号
if(sysUsers.getUser_type() < 0){
authorizationInfo.addRole("*"); // roles的权限 所有
authorizationInfo.addStringPermission("*:*:*"); // perms的权限 所有
} else {
// 用角色id从数据库获取权限列表,这里是模拟的
List<String> mapList = ShiroDataMapper.listSysRolesPermissions(sysUsers.getRole_id());
authorizationInfo.addRole("key");
if (!mapList.isEmpty()) {
Set<String> permsSet = new HashSet<>();
for (String perm : mapList) {
permsSet.addAll(Arrays.asList(perm.trim().split(",")));
}
authorizationInfo.setStringPermissions(permsSet);
}
}
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取登录username
String username = (String)token.getPrincipal();
// 从数据库获取用户对象 (这里模拟的)
SysUsers sysUsers = ShiroDataMapper.getSysUsersByUserName(username);
// 在数据库里没找到用户,异常用户,抛出异常(交给异常处理)
if(sysUsers == null) {
throw new UnknownAccountException(); //没找到帐号
}
// 一般用户允不允许登录也是有一个锁定状态的 从用户对象里拿到锁定状态,判断是否锁定
if(2 == sysUsers.getLocked()) {
throw new LockedAccountException(); //帐号锁定
}
// 交给SimpleAuthenticationInfo去验证密码
return new SimpleAuthenticationInfo(sysUsers, sysUsers.getPassword(), this.getClass().getName());
}
}
Shiro框架的ShiroConfig.java
SpringBoot集成Shiro有一个最主要的配置类,这个类里有Shiro框架的会话管理(
SessionManager
)和安全管理(
SecurityManager
)和
访问过滤器(ShiroFilterFactoryBean)和SpringBoot注解支持和生命周期相关的Bean配置
@Configuration必须加上的!
@Configuration
public class ShiroConfig {
}
ShiroConfig里首先来配置密码校验的bean
// 密码校验bean
@Bean("credentialMatcher")
public ShiroCredentialMatcher credentialMatcher() {
return new ShiroCredentialMatcher();
}
密码校验继承类ShiroCredentialMatcher.java
这里继承了SimpleCredentialsMatcher 实现方式是将登录的密码和数据库查询出来的密码进行一个equals对比,使用这种方式,密码可以是明码进行对比,也可以MD5后的密码,
同样的登录密码和数据库内的密码也可以在这里分别经过各自某种加密解密后在对比(安全系数瞬间增强,即使从数据库拿到了密码也没法简单确认出登录密码)
package boot.example.shiro.config;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
/**
* 蚂蚁舞
*/
public class ShiroCredentialMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String password = new String(usernamePasswordToken.getPassword());
String dbPassword = (String) info.getCredentials();
System.out.println("usernamePasswordToken--"+usernamePasswordToken.getUsername()+"--"+password);
System.out.println("info.getCredentials()-"+info.getCredentials()+"---info.getPrincipals()-"+info.getPrincipals());
// 密码比对
return this.equals(password, dbPassword);
}
}
SimpleCredentialsMatcher的源码
身份认证和权限校验Realm的bean
ShiroRealm就是授权和认证的类,设置的缓存管理使用的是内存,setCredentialsMatcher就是密码校验,MemoryConstrainedCacheManager缓存在内存中(方便快捷)
// 身份认证和权限校验Realm
@Bean("shiroRealm")
public ShiroRealm shiroRealm(@Qualifier("credentialMatcher") ShiroCredentialMatcher matcher){
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCacheManager(new MemoryConstrainedCacheManager());
shiroRealm.setCredentialsMatcher(matcher);
return shiroRealm;
}
SessionManager会话管理
shiro的会话管理SessionManager是用来管理应用中所有 Subject 的会话的创建、维护、删除、失效、验证,有三个默认的实现类
DefaultSessionManager
DefaultWebSessionManager:用于web环境的实现
ServletContainerSessionManager
shiro默认的会话管理是依赖于浏览器的cookie来维持的,也就是说前端代码嵌入到了SpringBoot整合Shiro的环境中,Shiro的会话管理将sesionId 放到 cookie中,现在大多数项目都是前后端分离的,去拿cookie还不如用token机制,一种无状态的机制,在登录的时候获取的token实际上就是shiro的sessionId,如此的话,那么可以继承实现DefaultWebSessionManager类,修改一些需要改变的方法
// 会话管理, 管理用户登录后的会话
@Bean("sessionManager")
public ShiroSessionManager sessionManager(){
//将继承后重写的ShiroSessionManager加入bean
return new ShiroSessionManager();
}
token的静态类ShiroConstant.java
package boot.example.shiro.config;
/**
* 蚂蚁舞
*/
public class ShiroConstant {
// 定义的请求头中使用的标记key,用来传递 token
public static final String authorization_token = "token";
}
重写会话管理类ShiroSessionManager.java
package boot.example.shiro.config;
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;
/**
* 蚂蚁舞
*/
public class ShiroSessionManager extends DefaultWebSessionManager {
public ShiroSessionManager() {
super();
//在这里设置ShiroSession失效时间
setGlobalSessionTimeout(MILLIS_PER_MINUTE * 15);
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//获取请求头中的token值,如果请求头中有token值,则取巧认为其值为会话的sessionId(那么用户在登陆的时候需要给前端传送这个sessionId)
String sessionId = WebUtils.toHttp(request).getHeader(ShiroConstant.authorization_token);
System.out.println("sessionId--" + sessionId);
if (StringUtils.isEmpty(sessionId)){
/**
* 注意: 在这里有一种特殊情况,那就是不经过shiroFilter过滤器的访问,例如authc认证用户
* 既然不经过shiroFilter 那么当后端重启清空了会话,可前端依旧把sessionId传给了后端,
* 出现这种情况,shiro会按照shiroFilterFactoryBean.setLoginUrl("/shiro-redirect/index");设置跳转到登录页面,重新登陆
* 格式是http://127.0.0.1:20400/shiro-redirect/index;JSESSIONID=04d5ed45-85c1-420b-b7bd-fa622385309f
* 如果是没有分离的项目,那么直接跳转到了登录页,如果是分离的项目,那就会给前端报出400的错误(这里是整合需要注意的关键点)
*/
//如果没有携带sessionId的参数,直接按照父类的方式在cookie进行获取sessionId
return super.getSessionId(request, response);
} else {
//请求头中如果有token, 则其值为sessionId(登陆的时候就传送这个sessionId)
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "request cookie");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); // 这里加上sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
}
}
}
看看
DefaultWebSessionManager父类getSessionId的源码
调用了私有
getReferencedSessionId方法
先调用this.getSessionIdCookieValue(request, response)获取sessionId 如果sessionid不存在,则去判断JSESSIONID的参数是不是带有(这个在前后端分离的项目有个大坑,不经过shiroFilter里的访问,接口会报出400错误,ShiroSessionManager的demo代码里有说明),暂时不去分析那么多,前后端分离一般也不会用到类似authc认证用户访问的,一般都是接口访问,有shiroFilter过滤器。
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
String id = this.getSessionIdCookieValue(request, response);
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "cookie");
} else {
id = this.getUriPathSegmentParamValue(request, "JSESSIONID");
if (id == null && request instanceof HttpServletRequest) {
String name = this.getSessionIdName();
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
String queryString = httpServletRequest.getQueryString();
if (queryString != null && queryString.contains(name)) {
id = request.getParameter(name);
}
if (id == null && queryString != null && queryString.contains(name.toLowerCase())) {
id = request.getParameter(name.toLowerCase());
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "url");
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
}
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, this.isSessionIdUrlRewritingEnabled());
return id;
}
SecurityManager安全管理器 Shiro框架的核心组件
// 安全管理器
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
// web的安全管理器
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 设置授权和认证
manager.setRealm(shiroRealm);
// 设置会话管理
manager.setSessionManager(sessionManager());
return manager;
}
ShiroFilterFactoryBean
访问过滤器(经常说成是拦截器,实际上是拦截的功能)
// 访问shiro的过滤器
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("shiroFilter", new ShiroFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 跳转到登录页,实际跳转后访问的是接口,接口返回请登录的信息
shiroFilterFactoryBean.setLoginUrl("/shiro-redirect/index");
//bean.setSuccessUrl("/shiro-redirect/index");
// 实际跳转到未认证页面,请重新登陆
shiroFilterFactoryBean.setUnauthorizedUrl("/shiro-redirect/unauthorized");
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 静态路径放开 anon:匿名用户可访问
filterChainDefinitionMap.put("/public/**", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
// 调试工具全部放开 anon:匿名用户可访问
filterChainDefinitionMap.put("/swagger-resources", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
// 登录相关全部放开 anon:匿名用户可访问
filterChainDefinitionMap.put("/shiro-login/**", "anon");
filterChainDefinitionMap.put("/shiro-redirect/**", "anon");
// 匿名用户可访问 anon:匿名用户可访问
filterChainDefinitionMap.put("/shiro-anon/**", "anon");
// 认证用户可访问 authc:认证用户可访问
filterChainDefinitionMap.put("/shiro-authc/*", "authc");
// 自定义过滤器过滤的内容
filterChainDefinitionMap.put("/**", "shiroFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
自定义过滤的类ShiroFilter.java
package boot.example.shiro.config;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
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;
import java.io.IOException;
/**
* 蚂蚁舞
*/
public class ShiroFilter extends BasicHttpAuthenticationFilter {
// sendChallenge重写的目的是避免前端在没有登录的情况下访问@RequiresPermissions()等未授权接口返回401错误,
// 给前端调用接口一个数据,让前端去重新登陆
// 如果使用浏览器访问,浏览器会弹出一个输入账号密码的弹框,重写后浏览器访问出现接口数据
protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
System.out.println("Authentication required: sending 401 Authentication challenge response.");
HttpServletResponse httpResponse = WebUtils.toHttp(response);
responseSkip(httpResponse, ResponseCode.noLoginSkipResponse());
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;
}
// 在配置的ShiroFilterFactoryBean拦截过滤器里,必须使用无状态的token 这里如果没有token 直接告诉前端需要重新登陆
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader(ShiroConstant.authorization_token);
if(authorization == null || authorization.length() == 0){
// 未携带token 不需要提示前端自动跳转重新登陆
responseSkip(httpServletResponse, ResponseCode.noAuthHeaderTokenResponse("未携带token,请求无效"));
return false;
}
// 验证token的正确性
Subject subject = SecurityUtils.getSubject();
if(!subject.isAuthenticated()){
// token失效 提示前端需要自动跳转重新登陆
responseSkip(httpServletResponse, ResponseCode.invalidHeaderTokenSkipResponse());
return false;
}
return super.preHandle(request, response);
}
private void responseSkip(HttpServletResponse response, Response customizeResponse){
try {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
ObjectMapper objectMapper = new ObjectMapper();
String str = objectMapper.writeValueAsString(customizeResponse);
response.getWriter().println(str);
} catch (IOException e1) {
throw new RuntimeException(e1);
}
}
}
注解支持的bean配置
支持在SpringBoot在Controller使用@RequiresPermission()等标签注解以及配置shiro的生命周期
// 支持在SpringBoot的Controller使用@RequiresPermission()等标签注解 以及
@Bean("authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题 (没明白)
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
// 配置shiro的生命周期处理
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
ShiroConfig.java完整类
package boot.example.shiro.config;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.SecurityManager;
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.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 蚂蚁舞
*/
@Configuration
public class ShiroConfig {
// 密码校验bean
@Bean("credentialMatcher")
public ShiroCredentialMatcher credentialMatcher() {
return new ShiroCredentialMatcher();
}
// 身份认证和权限校验Realm
@Bean("shiroRealm")
public ShiroRealm shiroRealm(@Qualifier("credentialMatcher") ShiroCredentialMatcher matcher){
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCacheManager(new MemoryConstrainedCacheManager());
shiroRealm.setCredentialsMatcher(matcher);
return shiroRealm;
}
// 会话管理, 管理用户登录后的会话
@Bean("sessionManager")
public ShiroSessionManager sessionManager(){
//将继承后重写的ShiroSessionManager加入bean
return new ShiroSessionManager();
}
// 安全管理器
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
// web的安全管理器
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 设置授权和认证
manager.setRealm(shiroRealm);
// 设置会话管理
manager.setSessionManager(sessionManager());
return manager;
}
// 访问shiro的过滤器
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("shiroFilter", new ShiroFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 跳转到登录页,实际跳转后访问的是接口,接口返回请登录的信息
shiroFilterFactoryBean.setLoginUrl("/shiro-redirect/index");
//bean.setSuccessUrl("/shiro-redirect/index");
// 实际跳转到未认证页面,请重新登陆
shiroFilterFactoryBean.setUnauthorizedUrl("/shiro-redirect/unauthorized");
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 静态路径放开 anon:匿名用户可访问
filterChainDefinitionMap.put("/public/**", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
// 调试工具全部放开 anon:匿名用户可访问
filterChainDefinitionMap.put("/swagger-resources", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
// 登录相关全部放开 anon:匿名用户可访问
filterChainDefinitionMap.put("/shiro-login/**", "anon");
filterChainDefinitionMap.put("/shiro-redirect/**", "anon");
// 匿名用户可访问 anon:匿名用户可访问
filterChainDefinitionMap.put("/shiro-anon/**", "anon");
// 认证用户可访问 authc:认证用户可访问
filterChainDefinitionMap.put("/shiro-authc/*", "authc");
// 自定义过滤器过滤的内容
filterChainDefinitionMap.put("/**", "shiroFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
// 支持在SpringBoot的Controller使用@RequiresPermission()等标签注解 以及
@Bean("authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题 (没明白)
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
// 配置shiro的生命周期处理
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
SpringBoot整合Shiro的web应用需要Controller层来调用测试功能的
首先是ShiroConfig里设置的重定向类
BootShiroIndexRedirectController.java
package boot.example.shiro.controller;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 蚂蚁舞
*/
@Controller
@RequestMapping("/shiro-redirect")
public class BootShiroIndexRedirectController {
public Logger log = LoggerFactory.getLogger(this.getClass());
@RequestMapping("/index")
@ResponseBody
public Response index() {
log.warn("redirect index");
return ResponseCode.noLoginResponse();
}
@RequestMapping("/unauthorized")
@ResponseBody
public Response unauthorized() {
log.warn("redirect unauthorized");
return ResponseCode.unauthorizedPermissionResponse();
}
}
匿名游客访问类BootShiroTestAnonController.java
package boot.example.shiro.controller;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 蚂蚁舞
*/
@RestController
@RequestMapping(value="/shiro-anon")
public class BootShiroTestAnonController {
@GetMapping(value="/hello")
public Response anonHello() {
return ResponseCode.successResponse("匿名游客用户可访问");
}
}
已经认证也就是登录的用户访问类BootShiroTestAuthcController.java
package boot.example.shiro.controller;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value="/shiro-authc")
public class BootShiroTestAuthcController {
@GetMapping(value="/hello")
public Response authCHello() {
return ResponseCode.successResponse("你是认证用户,可访问此接口");
}
}
使用权限注解的类BootShiroTestSysUserController.java
package boot.example.shiro.controller;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.*;
/**
* 蚂蚁舞
*/
@RestController
@RequestMapping(value="/sysUser")
public class BootShiroTestSysUserController {
@GetMapping(value="/hello")
public Response shiroFilterHello() {
return ResponseCode.successResponse("你正在访问登录后shiroFilter过滤器里的,无注解的接口");
}
@RequiresPermissions("sys:user:list")
@GetMapping(value="/list")
@ResponseBody
public Response userList() {
return ResponseCode.successResponse("你已经成功访问到查询用户接口");
}
@RequiresPermissions("sys:user:add")
@GetMapping(value="/insert")
@ResponseBody
public Response userAdd() {
return ResponseCode.successResponse("你已经成功访问到新增用户接口");
}
@RequiresPermissions("sys:user:update")
@GetMapping(value="/update")
@ResponseBody
public Response userUpdate() {
return ResponseCode.successResponse("你已经成功访问到更新用户接口");
}
@RequiresPermissions("sys:user:delete")
@GetMapping(value="/delete")
@ResponseBody
public Response userDelete() {
return ResponseCode.successResponse("你已经成功访问到删除用户接口");
}
}
登出类BootShiroLogoutController.java
package boot.example.shiro.controller;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 蚂蚁舞
*/
@Controller
@RequestMapping("/shiro-logout")
public class BootShiroLogoutController {
@GetMapping(value="/logout")
@ResponseBody
public Response logoutGet() {
Subject subject = SecurityUtils.getSubject();
if(subject != null){
subject.logout();
return ResponseCode.successResponse("登出成功");
}
return ResponseCode.failResponse("登出失败");
}
@PostMapping(value="/logout")
@ResponseBody
public Response logoutPost() {
Subject subject = SecurityUtils.getSubject();
if(subject != null){
subject.logout();
return ResponseCode.successResponse("登出成功");
}
return ResponseCode.failResponse("登出失败");
}
}
登录使用的类BootShiroLoginController.java
package boot.example.shiro.controller;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import boot.example.shiro.domain.SysUsers;
import boot.example.shiro.domain.UserValidate;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
/**
* 蚂蚁舞
*/
@Controller
@RequestMapping("/shiro-login")
public class BootShiroLoginController {
@GetMapping(value="/auth")
@ResponseBody
public Response authGet(@RequestParam(value = "username", required = true, defaultValue="shiro_admin") String username, @RequestParam(value = "password", required = true, defaultValue="123") String password) {
UserValidate userValidate = new UserValidate();
userValidate.setPassword(password);
userValidate.setUsername(username);
UsernamePasswordToken token = new UsernamePasswordToken(userValidate.getUsername(), userValidate.getPassword());
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
SysUsers sysUsers = (SysUsers) subject.getPrincipal();
Map<String, Object> map = new HashMap<>();
map.put("token", subject.getSession().getId().toString());
map.put("session", subject.getSession());
map.put("sysUsers", sysUsers);
return ResponseCode.successResponse(map);
} catch ( UnknownAccountException uae ) {
return ResponseCode.failResponse("error username");
} catch ( IncorrectCredentialsException ice ) {
return ResponseCode.failResponse("error password");
} catch ( LockedAccountException lae ) {
return ResponseCode.failResponse("locked user");
}
}
@PostMapping(value="/auth")
@ResponseBody
public Response authPost(@RequestBody UserValidate userValidate, HttpSession session) {
System.out.println(userValidate.toString());
UsernamePasswordToken token = new UsernamePasswordToken(userValidate.getUsername(), userValidate.getPassword());
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
SysUsers sysUsers = (SysUsers) subject.getPrincipal();
Map<String, Object> map = new HashMap<>();
map.put("token", subject.getSession().getId().toString());
map.put("session", subject.getSession());
map.put("sysUsers", sysUsers);
return ResponseCode.successResponse(map);
} catch ( UnknownAccountException uae ) {
return ResponseCode.failResponse("error username");
} catch ( IncorrectCredentialsException ice ) {
return ResponseCode.failResponse("error password");
} catch ( LockedAccountException lae ) {
return ResponseCode.failResponse("locked user");
}
}
}
SpringBoot整合Shiro的代码里有抛出异常的情况,主要的异常在登录的时候会在try catch里处理,返回给前端,但还是有些异常是捕获不到的,因此需要加上异常处理
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
// shiro 未授权异常
@ExceptionHandler(UnauthorizedException.class)
@ResponseBody
public Response UnauthorizedExceptionHandler(HttpServletRequest request, UnauthorizedException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.unauthorizedPermissionResponse("未授权,您的操作权限不够,可联系管理员获取操作权限");
}
// shiro 授权异常
@ExceptionHandler(AuthorizationException.class)
@ResponseBody
public Response AuthorizationException(HttpServletRequest request, AuthorizationException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse( "授权用户不存在或已经过期,请重新登录");
}
// shiro 未经身份验证或身份验证异常
@ExceptionHandler(UnauthenticatedException.class)
@ResponseBody
public Response UnauthenticatedException(HttpServletRequest request, UnauthenticatedException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("未经身份验证,身份验证异常,请重新登录");
}
// shiro 账号锁定异常
@ExceptionHandler(LockedAccountException.class)
@ResponseBody
public Response LockedAccountException(HttpServletRequest request, LockedAccountException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("你的账号已锁定,请联系管理员解锁");
}
// shiro 未找到用户异常
@ExceptionHandler(UnknownAccountException.class)
@ResponseBody
public Response UnknownAccountException(HttpServletRequest request, UnknownAccountException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("你的账号不存在");
}
// shiro 登录用户密码校验异常
@ExceptionHandler(IncorrectCredentialsException.class)
@ResponseBody
public Response IncorrectCredentialsException(HttpServletRequest request, IncorrectCredentialsException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("你输入的密码错误");
}
完整的异常处理类GlobalExceptionHandler.java
package boot.example.shiro.config;
import boot.example.shiro.domain.Response;
import boot.example.shiro.domain.ResponseCode;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.authz.permission.InvalidPermissionStringException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.bind.BindException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
/**
* 蚂蚁舞
*/
@ControllerAdvice
public class GlobalExceptionHandler {
public Logger log = LoggerFactory.getLogger(this.getClass());
// 全局异常:默认异常
@ExceptionHandler(Exception.class)
@ResponseBody
public Response defaultExceptionHandler(HttpServletRequest request, Exception e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.exceptionResponse(request.getRequestURI()+e.toString());
}
@ExceptionHandler(BindException.class)
@ResponseBody
public Response bindExceptionHandler(HttpServletRequest request, BindException e) {
return ResponseCode.exceptionResponse(e.toString());
}
// 全局异常:请求header缺少HeaderToken
@ExceptionHandler(ServletRequestBindingException.class)
@ResponseBody
public Response ServletRequestBindingExceptionHandler(HttpServletRequest request, ServletRequestBindingException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.noAuthHeaderTokenResponse();
}
// 全局异常:请求内容类型异常
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
@ResponseBody
public Response HttpMediaTypeNotSupportedExceptionHandler(HttpServletRequest request, HttpMediaTypeNotSupportedException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.exceptionResponse(e.toString());
}
// 全局异常:请求方法异常
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseBody
public Response HttpRequestMethodNotSupportedExceptionHandler(HttpServletRequest request, HttpRequestMethodNotSupportedException e) {
log.error(request.getRequestURI() +"----"+e.toString());
return ResponseCode.exceptionResponse(e.toString());
}
// 全局异常:请求参数格式或者参数类型不正确异常
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
public Response HttpMessageNotReadableExceptionHandler(HttpServletRequest request, HttpMessageNotReadableException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.exceptionResponse(e.toString());
}
// shiro 权限不可用
@ExceptionHandler(InvalidPermissionStringException.class)
@ResponseBody
public Response InvalidPermissionStringException(HttpServletRequest request, IncorrectCredentialsException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.notPermissionResponse("你的权限不可用");
}
// shiro 未授权异常
@ExceptionHandler(UnauthorizedException.class)
@ResponseBody
public Response UnauthorizedExceptionHandler(HttpServletRequest request, UnauthorizedException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.unauthorizedPermissionResponse("未授权,您的操作权限不够,可联系管理员获取操作权限");
}
// shiro 授权异常
@ExceptionHandler(AuthorizationException.class)
@ResponseBody
public Response AuthorizationException(HttpServletRequest request, AuthorizationException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse( "授权用户不存在或已经过期,请重新登录");
}
// shiro 未经身份验证或身份验证异常
@ExceptionHandler(UnauthenticatedException.class)
@ResponseBody
public Response UnauthenticatedException(HttpServletRequest request, UnauthenticatedException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("未经身份验证,身份验证异常,请重新登录");
}
// shiro 账号锁定异常
@ExceptionHandler(LockedAccountException.class)
@ResponseBody
public Response LockedAccountException(HttpServletRequest request, LockedAccountException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("你的账号已锁定,请联系管理员解锁");
}
// shiro 未找到用户异常
@ExceptionHandler(UnknownAccountException.class)
@ResponseBody
public Response UnknownAccountException(HttpServletRequest request, UnknownAccountException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("你的账号不存在");
}
// shiro 登录用户密码校验异常
@ExceptionHandler(IncorrectCredentialsException.class)
@ResponseBody
public Response IncorrectCredentialsException(HttpServletRequest request, IncorrectCredentialsException e) {
log.error(request.getRequestURI()+"----"+e.toString());
return ResponseCode.failResponse("你输入的密码错误");
}
}
跨域支持的BeanConfig.java
package boot.example.shiro.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 蚂蚁舞
*/
@Configuration
public class BeanConfig {
@Bean
public CorsFilter corsFilter(){
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("PUT");
corsConfiguration.addAllowedMethod("GET");
corsConfiguration.addAllowedMethod("POST");
corsConfiguration.addAllowedMethod("PATCH");
corsConfiguration.addAllowedMethod("OPTIONS");
corsConfiguration.addAllowedMethod("DELETE");
corsConfiguration.setMaxAge(1728000L);
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}
Response和静态类ResponseCode是统一封装的result结果集
public class Response {
private boolean state;
private int code;
private String msg;
private Object data;
private long timestamp;
// get set
}
完整的SpringBoot整合Shiro的代码结构
└─boot-example-shiro-separate-2.0.5
│ pom.xml
│
├─doc
│ boot-example-shiro-separate-2.0.5-back.zip
│
└─src
├─main
│ ├─java
│ │ └─boot
│ │ └─example
│ │ └─shiro
│ │ │ ShiroApp.java
│ │ │
│ │ ├─config
│ │ │ BeanConfig.java
│ │ │ GlobalExceptionHandler.java
│ │ │ ShiroConfig.java
│ │ │ ShiroConstant.java
│ │ │ ShiroCredentialMatcher.java
│ │ │ ShiroDataMapper.java
│ │ │ ShiroFilter.java
│ │ │ ShiroRealm.java
│ │ │ ShiroSessionManager.java
│ │ │ SwaggerConfig.java
│ │ │
│ │ ├─controller
│ │ │ BootShiroIndexRedirectController.java
│ │ │ BootShiroLoginController.java
│ │ │ BootShiroLogoutController.java
│ │ │ BootShiroTestAnonController.java
│ │ │ BootShiroTestAuthcController.java
│ │ │ BootShiroTestSysUserController.java
│ │ │
│ │ └─domain
│ │ Response.java
│ │ ResponseCode.java
│ │ SysUsers.java
│ │ UserValidate.java
│ │
│ └─resources
│ application.properties
│ logback-spring.xml
│
└─test
└─java
└─boot
└─example
└─shiro
ShiroAppTest.java
启动SpringBoot项目,访问swagger-ui(实际前后端分离的情况,这种方式也适合将前端代码嵌入到SpringBoot项目中)
浏览器和SwaggerUi测试
1.先不登录,访问匿名游客(anon)
2.先不登录,访问认证用户可访问(authc)这里有重定向
在浏览器直接访问/shiro-authc/hello 因为没有授权重定向到了 /shiro-redirect/index,但在接口上就看不到重定向操作了,直接得到数据未登录的结果
3.登录,使用预定的账号访问带注解的接口
app_admin 12345
这个账号登录访问的接口权限只有sys:user:list
也就是只能访问@RequiresPermissions(“sys:user:list”)
登录信息里除了用户信息还返回了token和session相关的信息,这个token就是前后端交互的
将token复制输入到token框里面,能够访问@RequiresPermissions(“sys:user:list”)
将token复制输入到token框里面,访问@RequiresPermissions(“sys:user:add”)权限不够
直接使用浏览器访问@RequiresPermissions(“sys:user:list”)注解的接口,发现需要token,那是因为前后端交互用的就是token机制,无状态的
4.当浏览器或swagger-ui上登录后,后端SpringBoot项目重启,访问
可以看到登录后使用浏览器直接输入会跳转,得到的确实是接口类型的数据,而不是显示的某个页面,但是在swagger-ui里,得到了JSESSIONID后面携带了token(sessionId)感觉像是没有实现前后端分离,这里之前在ShiroSessionManager类提到了的,尽量将所有接口都放在shiroFilter过滤器里,就是不使用authc这些
将这个注释掉
// 认证用户可访问 authc:认证用户可访问
filterChainDefinitionMap.put(“/shiro-authc/*”, “authc”);
其他使用浏览器和SwaggerUI测试的就不截图了(能避免的坑几乎都避免了,比如shiro的弹窗登录)
前后端交互要正真测试出能不能用,关键还是要使用独立的前端代码,这样才能测试真正的效果,SpringBoot整合Shiro和Vue实现前后端分离web项目最常见,这里使用vue+elementui搭建的测试程序进行测试
使用Vue前后端分离的前端测试Demo
使用vue和elementui来简单测试建立一个.vue文件就可以,核心代码首先定义request.js
import axios from 'axios'
import { getToken} from '@/utils/cookies'
import { Notification } from 'element-ui'
const service = axios.create({
baseURL: "http://127.0.0.1:20400",
timeout: 60000,
headers: {'Content-Type': 'application/json;charset=UTF-8'}
})
// 统一请求拦截器
service.interceptors.request.use(
config => {
if("/shiro-login/auth" === config.url){
return config
}
// 把token给后端
config.headers['token'] = getToken()
if(getToken()){
config.headers['token'] = getToken()
} else {
Notification({title: '消息',message: 'token失效,请重新登陆',type: 'warning',offset: 40})
return
}
return config
},
error => {
// 请求出错
console.log(error)
return Promise.reject(error)
}
)
// 统一响应拦截器
service.interceptors.response.use (
response => {
let res
// IE9时response.data是undefined,因此需要使用response.request.responseText(Stringify后的字符串)
if (response.data == undefined) {
res = JSON.parse(response.request.responseText)
} else {
res = response.data
}
//console.log(res);
//响应的逻辑判断
if(res){
return res
}
return Promise.reject(new Error("请求错误" || 'Error'))
},
error => {
//响应出错
console.log('err' + error)
return Promise.reject(error)
}
)
export default service
定义api接口shiroVue.js
import request from '@/axios/request'
// 登录
export const login = (data) => {
return request({
url: '/shiro-login/auth',
method: 'post',
data
})
}
// 登出
export const logout = () => {
return request({
url: '/shiro-logout/logout',
method: 'post'
})
}
// 匿名游客
export function anonHello() {
return request({
url: '/shiro-anon/hello',
method: 'get'
})
}
// 认证用户
export function authcHello() {
return request({
url: '/shiro-authc/hello',
method: 'get'
})
}
// 查询用户
export function userList() {
return request({
url: '/sysUser/list',
method: 'get'
})
}
// 新增用户
export function userInsert() {
return request({
url: '/sysUser/insert',
method: 'get'
})
}
// 更新用户
export function userUpdate() {
return request({
url: '/sysUser/update',
method: 'get'
})
}
// 删除用户
export function userDelete() {
return request({
url: '/sysUser/delete',
method: 'get'
})
}
需要用到cookie cookies.js
/**
* token认证
*
*/
import Cookies from 'js-cookie'
const mywTokenKey = 'mywToken'
export function getToken() {
return Cookies.get(mywTokenKey)
}
export function setToken(token) {
return Cookies.set(mywTokenKey, token)
}
export function removeToken() {
return Cookies.remove(mywTokenKey)
}
主要的测试代码Home.vue
<template>
<div style="border-radius:4px;padding:4px;">
<el-row style="padding-top:40px;">
<el-col :span="24">
<div>SpringBoot+Shiro框架整合实现前后端分离的权限管理基础Demo</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-input placeholder="用户账号" style="width:200px;margin-right:8px;" v-model="username" clearable></el-input>
<el-input placeholder="用户密码" style="width:200px;margin-right:8px;" v-model="password" clearable></el-input>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button type="info" @click="handleLogin()">登录系统</el-button>
<div style="height:4px;">{{ resultLogin }}</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button @click="handleLoGout()">登出系统</el-button>
<div style="height:4px;">{{ resultLogout }}</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button @click="handleanonHello()">匿名游客</el-button>
<div style="height:4px;">{{ resultAnonHello }}</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button type="info" @click="handleuserList()">查询用户</el-button>
<div style="height:4px;">{{ resultuserList }}</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button type="warning" @click="handleuserInsert()">新增用户</el-button>
<div style="height:4px;">{{ resultuserInsert }}</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button @click="handleuserUpdate()" type="success">编辑用户</el-button>
<div style="height:4px;">{{ resultuserUpdate }}</div>
</el-col>
<el-col :span="24" style="margin-top: 20px;">
<el-button @click="handleuserDelete()">删除用户</el-button>
<div style="height:4px;">{{ resultuserDelete }}</div>
</el-col>
<el-col :span="24" style="margin-top: 30px;">
<el-button type="info" @click="handleauthcHello()">认证访问(特殊)</el-button>
<div style="height:4px;">{{ resultAuthcHello }}</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { login, logout, anonHello, authcHello, userList, userInsert, userUpdate, userDelete} from '@/api/modules/shiroVue'
import {setToken, removeToken } from '../utils/cookies'
export default {
name: 'Home',
data() {
return {
username: "shiro_admin",
password: "123",
resultLogin: "",
resultLogout: "",
resultAnonHello: "",
resultAuthcHello: "",
resultuserList: "",
resultuserInsert: "",
resultuserUpdate: "",
resultuserDelete: ""
}
},
created() {
},
methods: {
init() {
},
handleLogin(){
if(this.username && this.password){
var data = {username: this.username, password: this.password}
login(data).then((response) => {
console.log(response)
if(response.state){
setToken(response.data.token)
this.resultLogin = "msg:"+response.msg+" token:"+ response.data.token
} else {
this.resultLogin = "msg:"+ response.msg
}
}).catch(response => {
console.log(response);
});
}
},
handleLoGout(){
logout().then((response) => {
console.log(response)
removeToken()
this.resultLogout = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
},
handleanonHello(){
anonHello().then((response) => {
console.log(response)
this.resultAnonHello = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
},
handleauthcHello(){
authcHello().then((response) => {
console.log(response)
this.resultAuthcHello = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
},
handleuserList(){
userList().then((response) => {
console.log(response)
this.resultuserList = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
},
handleuserInsert(){
userInsert().then((response) => {
console.log(response)
this.resultuserInsert = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
},
handleuserUpdate(){
userUpdate().then((response) => {
console.log(response)
this.resultuserUpdate = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
},
handleuserDelete(){
userDelete().then((response) => {
console.log(response)
this.resultuserDelete = "msg:"+ response.msg
}).catch(response => {
console.log(response);
});
}
},
mounted() {
this.$nextTick(function () {
this.init()
})
},
watch: {
}
}
</script>
首先将前端请求的token先注释掉
1.不登录的情况下访问接口
在未登录,也没有前端携带token的情况下,可以看到匿名游客可以访问,使用注解
@RequiresPermissions(“*:*:*”)提示没有token,因为在shiroFilter过滤器里过滤了的,认证用户访问的接口提示未登录
2.登录的情况下访问接口(前端不携带token)
可以看到登录后,想要登出都不可能,因为登出也是需要token认证的,加了注解的四个接口也是没有token,只有特殊的认证访问显示的是未登录,因为这种情况是这个接口是不经过shiroFilter过滤器的,但是进了自定义的session会话ShiroSessionManager,在这里他从http请求的header里没拿到token,因此ssesionId是null,即使调用了父类方法,也是没有的,于是重定向了接口,在这里重定向到了
/shiro-redirect/index 如果后端重启,在这里也是同样的状态,因此不测试了。
将前端请求的token注释取消,就是前端请求后端接口携带token
1.不登录的情况下访问接口(这是正式环境不会出现的情况)
可以看到前端报错了,那是因为在前端缓存里token不存在,直接return了,不是return config,所以不请求后端数据。
2.登录的情况下访问接口(前端携带token,正常情况)
这种整套操作流程,登录和登出都没问题
3.前端登录后,后端程序重启后访问认证页面(因为后端使用的内存,内存里没了shiro相关的会话)
可以看到报了400错误,那个shiro处理跳转了登录页面,分离的前端无法处理的,一般我们碰不到这个错误的
4.使用只有部分权限的账号登录
可以看到加了注解的接口只有查询用户可以访问,其他的访问权限不够。
这里的SpringBoot+Shiro和Vue实现前后端分离的Demo实际上使用到了Session的sessionId作为token来操作,如果要对这个token要求严格,那么可以使用对sessionId二次加密,例如jwt方式,这样在session重写的会话管理里先对token解密后在放入会话里。