SpringBoot+Shiro框架整合实现前后端分离的权限管理基础Demo

记录一下使用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项目中)

http://localhost:20400/doc.html

浏览器和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解密后在放入会话里。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/6905.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【云原生进阶之容器】第五章容器运行时5.4--容器运行时之Firecracker

1 Firecracker诞生背景 近些年 AWS 非常推崇无服务器模式

用Cmake构建第一个C++项目

ps&#xff1a;由于工作需求&#xff0c;需要涉及到跨平台。 概念 Cmake是一个款平台的构建工具&#xff0c;可以自动生成各种不同平台和编译器的构建脚本&#xff0c;使得项目在不同平台和编译器下都能够正常构建和运行。 CMake有自己的一套语法&#xff0c;需要学习CMake的…

上海亚商投顾:两市成交创年内新高 人工智能再爆发

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 市场情绪三大指数今日高开高走&#xff0c;沪指震荡反弹逼近3300点&#xff0c;创业板指午后涨超1.7%&#xff0c;科创50指数…

springboot 配置文件、多环境配置、运行优先级

前言 提问&#xff1a;springboot项目&#xff0c;开发环境、测试环境和生产环境配置文件如何分开表示&#xff1f; 答&#xff1a;多profile文件方式 1、多环境配置&#xff08;profile&#xff09; 1.1、properties文件配置 application.properties&#xff1a;主配置文…

基于html+css的between布局

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

【算法系列之动态规划IV】完全背包

完全背包 解题思路 01背包的核心思路&#xff0c;为了保证每个物品仅被添加一次&#xff0c;01背包内嵌的循环是从大到小遍历。 for(int i 0; i < weight.size(); i) { // 遍历物品for(int j bagWeight; j > weight[i]; j--) { // 遍历背包容量dp[j] max(dp[j], dp…

【云原生】k8s集群命令行工具kubectl之应用部署命令详解

kubectl应用部署命令详解一、准备工作1.1、Replication Controller1.2、Deployment1.3、DaemonSet1.4、查看创建的svc和pod1.5、kubectl 命令自动补全设置二、应用部署命令2.1、diff2.2、apply2.3、replace2.4、rollout2.4.1、history2.4.2、pause2.4.3、resume2.4.4、restart2…

Java 常量池分析

Java常量池 常量池&#xff1a;存放所有常量 常量池是Class文件中内容最为丰富的区域。常量池对于Class文件中的字段和方法解析也有着至关重要的作用。 随着Java虚拟机的不断发展&#xff0c;常量池的内容也日渐丰富。可以说&#xff0c;常量池是整个Class文件的基石。 在版…

HMC717ALP3E-ASEMI代理ADI(亚德诺)车规级芯片HMC717ALP3E

编辑-Z HMC717ALP3E参数描述&#xff1a; 频率范围&#xff1a;4.8 - 6.0GHz 增益&#xff1a;12.5dB 噪声系数&#xff1a;1.3dB 输入回波损耗&#xff1a;8dB 输出回波损耗&#xff1a;13dB 1 dB压缩的输出功率&#xff08;P1dB&#xff09;&#xff1a;12dBm 饱和输…

2023首届大学生算法大赛 - 村庄

读题可以发现&#xff0c;如果两个村庄不能互相连通&#xff0c;那就算作一对 &#xff08;a<b&#xff09;。 显然是可以用floyd全局多源最短路来做的&#xff0c;如果不存在最短路&#xff0c;那么就是不能互通&#xff0c;但是这道题的数据范围N<10^5&#xff0c;跑f…

SpringCloud之Eureka原理分析与实战(注册与发现)

目录 1、从本质理解服务治理思想 2、为什么选择Spring Cloud服务治理组件 3、Spring Cloud Eureka服务发现 3.1 Eureka的优势 3.2 Eureka架构组成 3.3 搭建Eureka Server 实战 3.3.1 添加依赖 3.3.2 开启服务注册 3.3.3 添加YML配置 3.3.4 访问服务 3.4 搭建Eureka …

IT服务商服务运营方案--PIGOSS BSM +TOC 服务加工具的新型运维模式

该解决方案适用于各种数据中心端专业运维服务商&#xff0c;包括驻场服务商&#xff0c;MA服务商&#xff0c;ITO服务商&#xff0c;IDC服务商&#xff0c;云运维服务商等 PIGOSS 是专业服务商的共同选择 专业的服务团队离不开专业的技术平台和技术工具&#xff0c;PIGOSS TOC…

echarts柱状图

1、先展示效果图 2、直接上代码&#xff0c;copy代码进行调试便会懂&#xff08;导入echarts插件看之前的文章&#xff09; <template><div class"antigen-container"><div class"top-content"><span class"top-title"&g…

电商商品详情API接口,python请求示例说明

PC端商品详情接口&#xff0c;H5商品详情接口&#xff0c;APP商品详情接口&#xff0c;商品详情接口&#xff0c;商品销量接口&#xff0c;商品列表接口&#xff0c;商品属性接口&#xff0c;商品sku接口&#xff0c;商品评论接口&#xff0c;商品优惠价接口&#xff0c;商品历…

求职咨询Job Information

前言 加油 原文 求职咨询常用会话 ❶ I want to apply for a job which enables me to use my major. 我想要申请一个能用到我的专业知识的职业。 ❷ I have the capability of operating the computer. 我有操作电脑的能力。 ❸ My dream is to be an excellent interpret…

ts基础内容

javascript脚本语言简称ts为javascript进阶脚本语法 TypeScript是微软开发的一个开源的编程语言&#xff0c;通过在JavaScript的基础上添加静态类型定义构建而成。TypeScript通过TypeScript编译器或Babel转译为JavaScript代码&#xff0c;可运行在任何浏览器&#xff0c;任何操…

Python 字符串格式化高级用法

字符串格式化: 是在编程过程中&#xff0c;允许编码人员通过特殊的占位符&#xff0c;将相关对应的信息整合或提取的规则字符串。 python字符串格式化字符串的格式化常用的三种方式&#xff0c;分别是使用 %格式化&#xff0c;format方法格式化&#xff0c;fstring格式化。 传…

元数据管理概述

参考公众号文章&#xff1a;数据治理&#xff1a;元数据及元数据管理策略、方法和技术

走进小程序【一】什么是小程序?

文章目录&#x1f31f;前言&#x1f31f;发展史&#x1f31f;什么是[微信小程序](https://developers.weixin.qq.com/miniprogram/dev/framework/)&#xff1f;&#x1f31f;微信小程序能做什么&#xff1f;&#x1f31f;小程序发展前景和优势&#x1f31f;写在最后&#x1f31…

应用层 —— HTTPS协议

目录 1、HTTPS介绍 HTTP 与 HTTPS "加密" 是什么 常见的加密方式 对称加密 非对称加密 数据摘要 && 数据指纹 数字签名 2、HTTPS的工作过程探究 方案1 —— 只使用对称加密&#xff08;明文传输不可取&#xff09; 方案2 —— 只使用非对称加密&#xff08…