Loading

SpringBoot + Apache Shiro权限管理

之前配置过Spring + SpringMVC + JPA + Shiro后台权限管理 + VUE前台登录页面的框架,手动配置各种.xml,比较繁琐,前几天写了个SpringBootShiro的Demo,梳理一下思路,记录在这里。

首先就是设计表数据

sys_user,用户,唯一标识是UID,NICKNAME可以重复,PASSWORD通过MD5加密算法计算后存入库。

uid nickname password
1111 admin dac97d365ee3fd3c7058b1255ceabeca
2222 John 4cc88c5148cfb401817395e1755ce31c

sys_role,角色。

rid sn description
1 Admin 超级管理员
2 Member 会员,普通注册用户

sys_permission,权限。

pid sn url description
1 admin:* /admin/** 超级管理
2 member:* /member/** 注册会员自我管理

用户、角色和权限表设计完,接下来就是设计两个中间表将他们联系起来。

sys_user_role,用户-角色 中间表。

uid rid
1111 1
2222 2

sys_role_permission,角色-权限 中间表。

rid pid
1 1
2 2

新建 SpringBoot 工程 SpringBootShiro

pom.xml 依赖

<dependencies>
    <!--SpringBoot-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>${org.springframework.boot.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>${org.springframework.boot.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <version>${org.springframework.boot.version}</version>
    </dependency>
    <!--MyBatis-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
        <version>${org.springframework.boot.version}</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.6</version>
    </dependency>
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.0.1</version>
    </dependency>
    <!--MySQL数据库驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.driver.version}</version>
    </dependency>
    <!--Apache Shiro-->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>${org.apache.shiro.version}</version>
    </dependency>
</dependencies>

工程启动入口 App.java

package net.add1s;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author lalafaye
 */
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

配置数据源,此工程选择 MyBatis 作为持久化框架

在 src/main/java 下和 resources 下建好相应的持久层包

然后 resources 下新建数据源配置文件 datasource/datasource.properties

db.mysql.jdbc-url=jdbc:mysql://localhost:3306/writer
db.mysql.username=root
db.mysql.password=password
db.mysql.driverClassName=com.mysql.jdbc.Driver

新建 config 包,注入 Bean 进行配置

package net.add1s.config;

import com.github.pagehelper.PageInterceptor;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;
import java.util.Properties;

/**
 * @author lalafaye
 */
@Configuration
@MapperScan(basePackages = MysqlSourceConfig.PACKAGE, sqlSessionFactoryRef = "mysqlSqlSessionFactory")
@PropertySource(value = "classpath:datasource/datasource.properties")
public class MysqlSourceConfig {

    static final String PACKAGE = "net.add1s.mapper.mysql";

    private static final String MAPPER_LOCATION = "classpath:mapper/mysql/*.xml";

    @Value("${db.mysql.jdbc-url}")
    private String url;

    @Value("${db.mysql.username}")
    private String user;

    @Value("${db.mysql.password}")
    private String password;

    @Value("${db.mysql.driverClassName}")
    private String driverClass;

    @Bean(name = "mysqlDataSource")
    @ConfigurationProperties(prefix = "db.mysql")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "mysqlTransactionManager")
    public DataSourceTransactionManager oracleTransactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }

    @Bean(name = "mysqlSqlSessionFactory")
    @Primary
    public SqlSessionFactory oracleSessionFactory(
            @Qualifier("mysqlDataSource") DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources(MysqlSourceConfig.MAPPER_LOCATION));
        // 添加pageHelper插件
        Interceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        // 数据库
        properties.setProperty("helperDialect", "oracle");
        // 是否将参数offset作为PageNum使用
        properties.setProperty("offsetAsPageNum", "true");
        // 是否进行count查询
        properties.setProperty("rowBoundsWithCount", "true");
        // 是否分页合理化
        properties.setProperty("reasonable", "true");
        properties.setProperty("supportMethodsArguments", "true");
        properties.setProperty("params", "count=countSql");
        interceptor.setProperties(properties);
        sessionFactory.setPlugins(new Interceptor[]{interceptor});
        return sessionFactory.getObject();
    }

    @Bean("mysqlSqlSessionTemplate")
    @Primary
    public SqlSessionTemplate mysqlSqlSessionTemplate(@Qualifier("mysqlSqlSessionFactory") SqlSessionFactory sessionFactory) {
        return new SqlSessionTemplate(sessionFactory);
    }
}

若想配置多数据源,请点击此处参考

开始配置 Shiro

新建包 entity,新建实体类 SysUser 和 SysPermission

package net.add1s.entity;

/**
 * @author lalafaye
 */
public class SysUser {

    /**
     * User id
     */
    private Long uid;

    /**
     * 昵称
     */
    private String nickname;

    /**
     * 密码
     */
    private String password;
    
    /** getter & setter here **/
}
package net.add1s.entity;

/**
 * @author lalafaye
 */
public class SysPermission {

    /**
     * Permission id
     */
    private Long pid;

    /**
     * 权限代码
     */
    private String sn;

    /**
     * 允许访问接口
     */
    private String url;

    /**
     * 描述
     */
    private String description;
    
    /** getter & setter here **/
}

xMapper.xml,持久层和业务层代码此处省略,仅作 SQL 记录。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://www.mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.add1s.mapper.mysql.SysUserMapper">
    <select id="findByUid" parameterType="java.lang.Long" resultType="net.add1s.entity.SysUser">
        SELECT uid, nickname, password FROM sys_user WHERE uid = #{ uid }
    </select>
    <select id="findByNickname" parameterType="java.lang.String" resultType="net.add1s.entity.SysUser">
        SELECT uid, nickname, password FROM sys_user WHERE nickname = #{ nickname }
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://www.mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.add1s.mapper.mysql.SysPermissionMapper">
    <select id="findAll" resultType="net.add1s.entity.SysPermission">
        SELECT * FROM sys_permission;
    </select>
    <select id="findByUid" parameterType="java.lang.Long" resultType="net.add1s.entity.SysPermission">
        SELECT p.*
        FROM sys_user u
        LEFT JOIN sys_user_role ur
        ON u.uid = ur.uid
        LEFT JOIN sys_role r
        ON ur.rid = r.rid
        LEFT JOIN sys_role_permission rp
        ON r.rid = rp.rid
        LEFT JOIN sys_permission p
        ON rp.pid = p.pid
        WHERE u.uid = #{ uid };
    </select>
    <select id="findSnByUid" parameterType="java.lang.Long" resultType="java.lang.String">
        SELECT p.sn
        FROM sys_user u
        LEFT JOIN sys_user_role ur
        ON u.uid = ur.uid
        LEFT JOIN sys_role r
        ON ur.rid = r.rid
        LEFT JOIN sys_role_permission rp
        ON r.rid = rp.rid
        LEFT JOIN sys_permission p
        ON rp.pid = p.pid
        WHERE u.uid = #{ uid };
    </select>
</mapper>

新建 MyRealm.java,继承 AuthorizingRealm,自定义认定器(自己写逻辑设置权限及登录验证)

package net.add1s.shiro;

import net.add1s.entity.SysUser;
import net.add1s.pojo.UserContext;
import net.add1s.service.SysPermissionService;
import net.add1s.service.SysUserService;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Set;

/**
 * @author mahoshojo
 */
public class MyRealm extends AuthorizingRealm {

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private SysPermissionService sysPermissionService;

    /**
     * 权限设置
     *
     * @param principalCollection
     * @return SimpleAuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        // 获取权限资源
        Set<String> sysPermissionSet = sysPermissionService.findSnByUid(UserContext.getUserFromSession().getUid());

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // 设置权限
        simpleAuthorizationInfo.setStringPermissions(sysPermissionSet);

        return simpleAuthorizationInfo;
    }

    /**
     * 登录验证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        // 转换为UsernamePasswordToken
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        Long uid = Long.valueOf(username);

        // 获取安全数据
        SysUser loginUser = sysUserService.findByUid(uid);
        if (loginUser == null) {
            return null;
        }
        String credentials = loginUser.getPassword();

        // 盐值
        ByteSource salt = ByteSource.Util.bytes(username);

        // 身份信息(自动比较获取的密码与前台传过来的密码)
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(loginUser, credentials, salt, "Lalafaye's Realm");

        return simpleAuthenticationInfo;
    }
}

在包 config 下新建 ShiroConfig.java

package net.add1s.config;

import net.add1s.entity.SysPermission;
import net.add1s.service.SysPermissionService;
import net.add1s.shiro.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
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.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @author lalafaye
 */
@Configuration
public class ShiroConfig {

    @Autowired
    private SysPermissionService sysPermissionService;

    /**
     * 对POST:/login传入的密码进行加密
     *
     * @return
     */
    @Bean("hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        // 加密
        return new HashedCredentialsMatcher() {{
            // 设置MD5加密方式
            setHashAlgorithmName("MD5");
            // 加密次数100
            setHashIterations(100);
            // 设置十六进制编码的存储凭据
            setStoredCredentialsHexEncoded(true);
        }};
    }

    /**
     * 配置认定器
     *
     * @param matcher
     * @return
     */
    @Bean("myRealm")
    public MyRealm myRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {
        return new MyRealm() {{
            // 禁用授权缓存
            setAuthorizationCachingEnabled(false);
            // 设置加密匹配器
            setCredentialsMatcher(matcher);
        }};
    }

    /**
     * 配置安全管理器,注入认定器Realm
     *
     * @param myRealm
     * @return
     */
    @Bean("securityManager")
    public SecurityManager securityManager(@Qualifier("myRealm") MyRealm myRealm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

        // 设置Realm
        defaultWebSecurityManager.setRealm(myRealm);

        return defaultWebSecurityManager;
    }

    /**
     * 配置过滤器,注入安全管理器
     *
     * @param securityManager
     * @return
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 设置默认登录接口,若不设置则默认寻找WEB工程的根目录下的login.jsp文件
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后跳转链接
        shiroFilterFactoryBean.setSuccessUrl("/u/index");
        // 未授权界面
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        // 拦截器,使用LinkedHashMap,保证拦截器作用的顺序和添加项目的顺序一致
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 未登录放行
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        // 权限过滤
        List<SysPermission> sysPermissions = sysPermissionService.find();
        sysPermissions.forEach(sysPermission -> filterChainDefinitionMap.put(sysPermission.getUrl(), "perms[" + sysPermission.getSn() + "]"));
        // 所有接口必须认证通过后才被放行,必须放在Map的最后之前添加的过滤器才会起作用
        filterChainDefinitionMap.put("/**", "authc");
        // 设置过滤器集
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    /**
     * Spring的一个bean, 由Advisor决定对哪些类的方法进行AOP代理
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }

    /**
     * Shiro关联Spring
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

MD5Utils.java,用于添加新用户时对密码进行加密

package net.add1s.util;

import org.apache.shiro.crypto.hash.SimpleHash;

/**
 * @author lalafaye
 */
public class MD5Util {

    /**
     * 默认盐值
     */
    private static final String DEFAULT_SALT = "lalafaye";

    /**
     * 加密次数
     */
    private static final int HASH_ITERATIONS = 100;

    /**
     * 返回盐值
     *
     * @return private static final String SALT 盐值
     */
    public static String getDefaultSalt() {
        return DEFAULT_SALT;
    }

    /**
     * 返回加密次数
     *
     * @return private static final int HASHITERATIONS 加密次数
     */
    public static int getHashIterations() {
        return HASH_ITERATIONS;
    }

    /**
     * 返回加密后的MD5字符串
     *
     * @param source 待加密源数据
     * @param salt 盐值,传null时默认使用“lalafaye”
     * @return 加密过后的MD5结果字符串
     */
    public static String createMd5String(String source, String salt) {
        if (salt == null) {
            return new SimpleHash("MD5", source, DEFAULT_SALT, HASH_ITERATIONS).toString();
        } else {
            return new SimpleHash("MD5", source, salt, HASH_ITERATIONS).toString();
        }
    }
}

表现层接口测试

新建包 pojo,AjaxResult & UserContext

package net.add1s.pojo;

/**
 * Ajax请求响应对象
 *
 * @author lalafaye
 */
public class AjaxResult {

    private boolean success = true;

    private String msg = "Successful operation!";

    private Object resultObj;

    public boolean isSuccess() {
        return success;
    }

    public AjaxResult setSuccess(boolean success) {
        this.success = success;
        return this;
    }

    public String getMsg() {
        return msg;
    }

    public AjaxResult setMsg(String msg) {
        this.msg = msg;
        return this;
    }

    public Object getResultObj() {
        return resultObj;
    }

    public AjaxResult setResultObj(Object resultObj) {
        this.resultObj = resultObj;
        return this;
    }

    /**
     * 成功:AjaxResult.me()
     * 成功:AjaxResult.me().setMsg("自定义提示消息").setResultObj(Object响应信息)
     * 失败:AjaxResult.me().setSuccess(false)
     *
     * @return
     */
    public static AjaxResult me() {
        return new AjaxResult();
    }
}
package net.add1s.pojo;

import net.add1s.entity.SysUser;
import org.apache.shiro.SecurityUtils;

/**
 * @author lalafaye
 */
public class UserContext {

    public static final String USER_IN_SESSION = "USER_IN_SESSION";

    /**
     * 设置用户到session
     *
     * @param user SysUser登录对象
     */
    public static void setUserToSession(SysUser user) {
        SecurityUtils.getSubject().getSession().setAttribute(USER_IN_SESSION, user);
    }

    /**
     * 从session中获取用户
     *
     * @return SysUser登录实体
     */
    public static SysUser getUserFromSession() {
        return (SysUser) SecurityUtils.getSubject().getSession().getAttribute(USER_IN_SESSION);
    }
}

新建包 controller,LoginController

package net.add1s.controller;

import net.add1s.entity.SysUser;
import net.add1s.pojo.AjaxResult;
import net.add1s.pojo.UserContext;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author lalafaye
 */
@RestController
public class LoginController {

    @PostMapping("/login")
    public AjaxResult login(String uid, String password) {

        Subject subject = SecurityUtils.getSubject();

        // 若当前没有主体或登录用户不同,则进行登录
        if (!subject.isAuthenticated() || !uid.equals(UserContext.getUserFromSession())) {
            // 先登出当前已登录用户
            SecurityUtils.getSubject().logout();
            UsernamePasswordToken token = new UsernamePasswordToken(uid, password);
            try {
                subject.login(token);
            } catch (UnknownAccountException | IncorrectCredentialsException e) {
                e.printStackTrace();
                return AjaxResult.me().setSuccess(false).setMsg("用户名或密码错误");
            } catch (AuthenticationException e) {
                e.printStackTrace();
                return AjaxResult.me().setSuccess(false).setMsg("未知错误,请联系管理员");
            }
        }

        // 登录成功后,把当前登录用户放到session中
        UserContext.setUserToSession((SysUser) subject.getPrincipal());

        return AjaxResult.me();
    }

    @GetMapping("/logout")
    public void logout() {
        SecurityUtils.getSubject().logout();
    }
}

resources/application.yml 配置端口

server:
  port: 9527

打开 POSTMAN 进行测试

POST 请求接口 http://localhost:9527/login

KEY VALUE
uid 1111
password admin

响应

{
    "success": true,
    "msg": "Successful operation!",
    "resultObj": null
}

登录成功

修改 uid 或 password 后响应

{
    "success": false,
    "msg": "用户名或密码错误",
    "resultObj": null
}

关于登录成功后的权限测试此处省略,有兴趣的同学请自行测试


此工程已经上传 Github

posted @ 2019-10-09 21:45  mahoshojo  阅读(423)  评论(0编辑  收藏  举报