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