Shiro SpringBoot整合——快速开发出你的安全Web应用

1,016 阅读7分钟

给自己的Web框架添加合适的安全框架是一件很重要的事,在这里,我选择了Shiro,因为它更加简单且通俗易懂,现在来看看怎么把它和SpringBoot整合到一块吧!

关于Shiro基本原理,可以看这个

核心依赖

<dependency>
    <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>${your.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
    <version>${your.version}</version>
</dependency>

在这里还要添加数据库,mybatis,web等依赖

数据库创建

1.sys_user

CREATE TABLE `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(25) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `salt` varchar(25) DEFAULT NULL,
  `state` int DEFAULT '0',
  PRIMARY KEY (`id`)
);

对应的POJO

public class SysUser {

    private Long id;

    private String username;

    private String nickname;

    private String password;

    private String salt;

    private Integer state;
}

2.sys_role

CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(25) DEFAULT NULL,
  `desc` varchar(25) DEFAULT NULL,
  `is_available` tinyint(1) DEFAULT '0',
  PRIMARY KEY (`id`)
);

对应的POJO

public class SysRole {

    private Long id;

    /**
     * 角色名
     */
    private String name;

    private String desc;

    private Boolean isAvailable = Boolean.FALSE;
}

3.sys_permission

CREATE TABLE `sys_permission` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(25) DEFAULT NULL,
  `desc` varchar(25) DEFAULT NULL,
  `resource_type` varchar(25) DEFAULT NULL,
  `url` varchar(25) DEFAULT NULL,
  `parent_id` bigint DEFAULT NULL,
  `parent_ids` varchar(25) DEFAULT NULL,
  `is_available` tinyint(1) DEFAULT '0',
  PRIMARY KEY (`id`)
);

对应的POJO

public class SysPermission {

    private Long id;

    /**
     * 许可名
     */
    private String name;

    /**
     * 资源类型
     */
    private String resourceType;

    /**
     * 资源url
     */
    private String url;

    /**
     * 许可字符串
     */
    private String desc;

    /**
     * 父编号
     */
    private Long parentId;

    private String parentIds;

    private Boolean isAvailable = Boolean.FALSE;
}

4.sys_user_roles

CREATE TABLE `sys_user_roles` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint DEFAULT NULL,
  `role_id` bigint DEFAULT NULL,
  PRIMARY KEY (`id`)
);

对应的POJO

public class SysUserRoles {

    private Long id;

    private Long userId;

    private Long RoleId;
}

5.sys_user_permissions

CREATE TABLE `sys_user_permissions` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint DEFAULT NULL,
  `permission_id` bigint DEFAULT NULL,
  PRIMARY KEY (`id`)
);

对应的POJO

public class SysUserPermissions {

    private Long id;

    private Long userId;

    private Long permissionId;
}

6.sys_role_permissions

CREATE TABLE `sys_role_permissions` (
  `id` int NOT NULL AUTO_INCREMENT,
  `role_id` bigint DEFAULT NULL,
  `permission_id` bigint DEFAULT NULL,
  PRIMARY KEY (`id`)
);

public class SysRolePermissions {

    private Long id;

    private Long roleId;

    private Long permissionId;
}

注意哈!我是图简单,数据类型全用最简单的了,实际设计请根据需要设计!!!

基本目录结构以及设计

ShiroConfig

此类提供shiro的基本配置,比如ShiroFilterChainDefinition:用来定义拦截路径,这不同于集成SpringMVC的样子,登录路径以及登录成功,未验证的路径是在配置文件里设置的。

package com.learn.shiro.config;

import com.learn.shiro.dao.shiro.WebRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

/**
 * @author SuanCaiYv
 * @time 2020/6/9 下午4:27
 */
@Configuration
public class ShiroConfig {

    /**
     * 注入域对象,用来完成验证和授权操作
     */
    @Autowired
    private WebRealm webRealm;

    /**
     * 进一步根据访问路径配置更详细的过滤器,add()的第二个参数代表了过滤器的名字
     * 来看详细的解释
     * anon: AnonymousFilter,允许不做验证直接访问,相当于不加过滤器
     * authc: FormAuthenticationFilter,要求请求的用户必须是已验证的,否则强制重定向到设置好的登录界面
     * authcBasic: BasicHttpAuthenticationFilter,要求org.apache.shiro.subject.Subject#isAuthenticated()返回真,否则要求登录
     * authcBearer: BearerHttpAuthenticationFilter,和上面效果差不多,协议种类不同
     * logout: LogoutFilter,对当前用户进行登出操作,并重定向到设定好的“重定向Url”
     * noSessionCreation: NoSessionCreationFilter,禁用Session的创建,对于可能产生REST,SOAP等交互结果的操作很有用
     * perms: PermissionsAuthorizationFilter,如果当前用户拥有map指定的值时,允许访问
     * port: PortFilter把请求限定在某一指定端口,如果请求不在此端口发起请求,那么重定向到这个端口
     * rest: HttpMethodPermissionFilter,把HTTP请求方法转换成一个行为值,并用这个值构建一个许可用来进行授权验证
     * roles: RolesAuthorizationFilter,当当前用户持有map包含的值之一时,允许访问,如果持有的所有角色都没被包含,拒绝访问
     * ssl: SslFilter,如果是SSL加密的,允许,否则拒绝
     * user: UserFilter,如果是已知用户,允许访问,判别方法:principal是否被标记,或者说,任何已验证的,已被“记住”的均可访问
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition shiroFilterChainDefinition = new DefaultShiroFilterChainDefinition();
        shiroFilterChainDefinition.addPathDefinition("/doLogin", "anon");
        shiroFilterChainDefinition.addPathDefinition("/**", "authc");
        return shiroFilterChainDefinition;
    }

    /**
     * 设置SecurityManager
     */
    @PostConstruct
    public void init() {

        SecurityUtils.setSecurityManager(securityManager());
    }

    /**
     * 关于SecurityUtils.getSubject()的个人理解,因为tomcat是多线程的,所以每次获取的Subject是怎么保证是当前与应用交互的Subject的呢?
     * 答案就是ThreadLocal。这个类利用Map针对每个线程获取线程的独立于其他线程的域,怎么做到的呢?
     * 这个Map的key不是不同的,而是全部是ThreadLocal类,但是有多个Map,每个线程都保存了一个Map,每个Map都有一个key为ThreadLocal的键值对。
     * 这个和我们平时使用的Map有点不一样,平时想保存多个键值对,无非多个键对应多个值,这个是多个Map对应多个值,每个Map都有某一确定的值
     * 想获取不同的值不是通过改变key的方式而是,通过改变Map的方式来实现,而Map绑定在线程上,遂直接获取当前线程的Map就好。
     * 所以SecurityUtils.getSubject()是获取当前线程的Map的key为"ThreadContext_SUBJECT_KEY"的键值对的值来获取Subject的。
     * @return NA
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setSessionManager(sessionManager());
        securityManager.setCacheManager(cacheManager());
        securityManager.setRealm(webRealm);
        return securityManager;
    }

    /**
     * 设置会话管理器
     */
    @Bean
    public SessionManager sessionManager() {

        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        return sessionManager;
    }

    /**
     * 设置缓存管理器
     */
    @Bean
    public CacheManager cacheManager() {

        EhCacheManager ehCacheManager = new EhCacheManager();
        return ehCacheManager;
    }

    /**
     * 开启注解声明,不然会导致注解方法没法使用
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        return new DefaultAdvisorAutoProxyCreator();
    }

}

登录处理以及未验证处理Controller:

package com.learn.shiro.controller;

import com.learn.shiro.result.ResultBean;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author SuanCaiYv
 * @time 2020/6/9 下午9:12
 */
@RestController
public class ControllerOne {

    /**
     * 实现真正的登录功能
     */
    @RequestMapping("/doLogin")
    public ResultBean<Void> doLogin(String email, String password) {
        UsernamePasswordToken token = new UsernamePasswordToken();
        token.setUsername(email);
        token.setPassword(password.toCharArray());
        Subject subject = SecurityUtils.getSubject();
        ResultBean<Void> resultBean = new ResultBean<>();
        // 如果已有用户登录
        if (subject.isAuthenticated()) {
            return resultBean;
        }
        try {
            subject.login(token);
            token.setRememberMe(true);
            resultBean.setMsg("登录成功");
            resultBean.setCode(ResultBean.ALL_PASSED);
        } catch (IncorrectCredentialsException e) {
            resultBean.setMsg("密码错误");
            resultBean.setCode(ResultBean.INCORRECT_PASSWORD);
        } catch (ExpiredCredentialsException e) {
            resultBean.setMsg("密码过期");
            resultBean.setCode(ResultBean.LOCKED_USER);
        } catch (UnknownAccountException e) {
            resultBean.setMsg("用户未注册");
            resultBean.setCode(ResultBean.UNSIGNED_USER);
        }
        return resultBean;
    }

    /**
     * 登出设置
     */
    @RequestMapping("/logout")
    public ResultBean<Void> logout() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return new ResultBean<>();
    }

    /**
     * 设置未验证的映射路径
     */
    @RequestMapping("/unauth")
    public String error() {
        return "unauth";
    }

    /**
     * 设置未登录时的跳转路径
     */
    @RequestMapping("/login")
    public String  login() {
        return "please login!";
    }
}

权限测试Controller:

package com.learn.shiro.controller;

import com.learn.shiro.result.ResultBean;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.authz.permission.WildcardPermission;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author SuanCaiYv
 * @time 2020/6/10 下午8:43
 */
@RestController
public class ControllerTwo {

    /**
     * 要求操作者必须拥有角色身份,同时必须是已验证的,否则抛异常
     */
    @RequiresAuthentication
    @RequiresRoles(value = {"com", "adm", "hed"}, logical = Logical.OR)
    @RequestMapping("/task/lunch")
    public ResultBean<Void> f1() {
        Subject subject = SecurityUtils.getSubject();
        ResultBean<Void> resultBean = new ResultBean<>();
        if (subject.isPermitted(new WildcardPermission("*:lun_tsk"))) {
            resultBean.setCode(ResultBean.ALL_PASSED);
            resultBean.setMsg("发布成功");
        }
        else {
            resultBean.setCode(ResultBean.ACCESS_DENIED);
            resultBean.setMsg("您貌似没有权限");
        }
        return resultBean;
    }
}

验证与授权实现类:

package com.learn.shiro.dao.shiro;

import com.learn.shiro.pojo.shiro.SysUser;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.authz.permission.WildcardPermission;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author SuanCaiYv
 * @time 2020/6/10 下午11:22
 */
@Component
public class WebRealm extends AuthorizingRealm {

    private static final HashedCredentialsMatcher credentialsMatcher;

    private final SysUserMapper sysUserMapper;

    private final SysUserRolesMapper sysUserRolesMapper;

    private final SysUserPermissionsMapper sysUserPermissionsMapper;

    private final SysRoleMapper sysRoleMapper;

    private final SysPermissionMapper sysPermissionMapper;

    private final SysRolePermissionsMapper sysRolePermissionsMapper;


    /**
     * 这里的设置和你的加密方式一致
     */
    static {
        credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("MD5");
        credentialsMatcher.setHashIterations(1024);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
    }

    /**
     * 这里一定要设置setCredentialsMatcher(),不然会造成密码匹配失败
     */
    public WebRealm(@Autowired SysUserMapper sysUserMapper, @Autowired SysUserRolesMapper sysUserRolesMapper,
                    @Autowired SysUserPermissionsMapper sysUserPermissionsMapper, @Autowired SysRoleMapper sysRoleMapper,
                    @Autowired SysPermissionMapper sysPermissionMapper, @Autowired SysRolePermissionsMapper sysRolePermissionsMapper) {
        this.setCredentialsMatcher(credentialsMatcher);
        this.sysUserMapper = sysUserMapper;
        this.sysUserRolesMapper = sysUserRolesMapper;
        this.sysUserPermissionsMapper = sysUserPermissionsMapper;
        this.sysRoleMapper = sysRoleMapper;
        this.sysPermissionMapper = sysPermissionMapper;
        this.sysRolePermissionsMapper = sysRolePermissionsMapper;
    }


    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String) principals.getPrimaryPrincipal();
        SysUser sysUser = sysUserMapper.select(username);
        List<Long> roleIds = sysUserRolesMapper.selectByUser(sysUser.getId());
        List<Long> permissionIds = sysUserPermissionsMapper.selectByUser(sysUser.getId());
        Set<String> roles = new HashSet<>();
        Set<Permission> permissions = new HashSet<>();
        // 获取用户权限
        for (Long roleId : roleIds) {
            roles.add(sysRoleMapper.selectById(roleId).getName());
            for (Long permissionId : permissionIds) {
                WildcardPermission permission = new WildcardPermission(sysRoleMapper.selectById(roleId).getName()+
                        ":"+sysPermissionMapper.selectById(permissionId).getName());
                permissions.add(permission);
            }
        }
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        // 添加权限的过程
        simpleAuthorizationInfo.setRoles(roles);
        simpleAuthorizationInfo.setObjectPermissions(permissions);
        return simpleAuthorizationInfo;
    }

    /**
     * 验证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        String username = (String) token.getPrincipal();
        SysUser sysUser = sysUserMapper.select(username);
        if (sysUser == null) {
            throw new UnknownAccountException("Unsigned User!");
        }
        // 验证用户身份
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token.getPrincipal(), sysUser.getPassword(), ByteSource.Util.bytes(sysUser.getSalt()), this.getName());
        return simpleAuthenticationInfo;
    }
}

配置文件:

server:
  port: 8190
shiro:
  sessionManager:
    sessionIdCookieEnabled: true
    sessionIdUrlRewritingEnabled: true
  web:
    enabled: true
  successUrl: /index
  loginUrl: /login
  unauthorizedUrl: /unauth
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/shiro_test
    username: root
    password: root
mybatis:
  config-location: classpath:mybatis-config.xml
  mapper-locations: classpath:mappers/*.xml

最后贴上添加用户数据的操作:

package com.learn.shiro;

import com.learn.shiro.dao.shiro.SysPermissionMapper;
import com.learn.shiro.dao.shiro.SysRoleMapper;
import com.learn.shiro.dao.shiro.SysUserMapper;
import com.learn.shiro.pojo.shiro.SysPermission;
import com.learn.shiro.pojo.shiro.SysUser;
import com.learn.shiro.util.BaseUtil;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ShiroApplicationTests {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private SysRoleMapper sysRoleMapper;

    @Autowired
    private SysPermissionMapper sysPermissionMapper;

    @Test
    void contextLoads() {

        SysUser sysUser = new SysUser();
        sysUser.setUsername("2021601470@qq.com");
        sysUser.setNickname("水煮鱼");
        sysUser.setState(0);
        String salt = BaseUtil.getSalt();
        sysUser.setSalt(salt);
        SimpleHash simpleHash = new SimpleHash("MD5", "123456", salt, 1024);
        sysUser.setPassword(simpleHash.toHex());
        sysUserMapper.insert(sysUser);
    }
}

至此,一个使用Shiro的SpringBoot应用就设置完成了!

来看看运行结果吧!

完整代码地址:github.com/SuanCaiYv/s…