一.简介
Shiro作为优秀的安全框架,包括了身份验证、授权、密码和会话管理,等功能。本文将介绍springboot集成Shiro的各个功能的具体操作。
Shiro使用的核心api有:
- Subject :与Shiro交互的主要类,相当于一个操作Shiro的用户
- SecurityManager :Shiro的核心,管理所有用户的操作
- Realm :身份验证和授权的数据获取层
- CredentialsMatcher :加密方式配置类
- AuthenticationToken :用于认证使用
- ShiroFilterFactoryBean :处理url授权和过滤
二.springboot集成Shiro
- 前期准备
使用Shiro前必须先建立user(用户),role(角色),permission(权限)3张主表和user_role(用户角色),role_permission(角色权限)2张关系表:
- 用户表
CREATE TABLE `user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) DEFAULT NULL COMMENT '密码',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`mobile` varchar(100) DEFAULT NULL COMMENT '手机号',
`salt` varchar(100) DEFAULT NULL COMMENT '盐',
`status` tinyint(4) DEFAULT NULL COMMENT '状态 0:禁用 1:正常',
`create_user_id` bigint(20) DEFAULT NULL COMMENT '创建者ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`delete_flag` int(2) NOT NULL DEFAULT '0' COMMENT '删除标识 -1=删除 0=正常',
PRIMARY KEY (`user_id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户';
- 角色表
CREATE TABLE `role` (
`role_id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_name` varchar(100) DEFAULT NULL COMMENT '角色名称',
`remark` varchar(100) DEFAULT NULL COMMENT '备注',
`create_user_id` bigint(20) DEFAULT NULL COMMENT '创建者ID',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色';
- 权限表
CREATE TABLE `permission` (
`permission_id` bigint(11) NOT NULL AUTO_INCREMENT,
`perms` varchar(20) DEFAULT NULL COMMENT '权限',
`name` varchar(20) DEFAULT NULL COMMENT '权限名',
`created_by` bigint(11) DEFAULT NULL COMMENT '创建人',
`created_date` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 用户角色表
CREATE TABLE `user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
`role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户与角色对应关系';
- 角色权限表
CREATE TABLE `sys_role_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',
`permission_id` bigint(20) DEFAULT NULL COMMENT '权限ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色与权限对应关系';
- 在pom.xml中添加shiro-spring依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.1</version>
</dependency>
最新依赖可以自行到maven仓库中找:https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring
3. 创建Realm数据层
如果不想创建自定义的Realm也可以用JdbcRealm的,我这里用一个自定义,灵活很多,也方便。
package com.zcx.demo.shiroweb.shiro.realm;
import com.zcx.demo.shiroweb.mapper.UserMapper;
import com.zcx.demo.shiroweb.mapper.RoleMapper;
import com.zcx.demo.shiroweb.mapper.PermissionMapper;
import com.zcx.demo.shiroweb.vo.User;
import org.apache.commons.collections.map.HashedMap;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.apache.shiro.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class CustomRealm extends AuthorizingRealm {
@Resource
UserMapper userMapper;
@Resource
RoleMapper roleMapper;
@Resource
PermissionMapper permissionMapper;
/**
*授权的方法
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) principalCollection.getPrimaryPrincipal();
List<String> roles = getRoleByName(username);
List<String> permissions = getPermissionsByRoles(roles);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setRoles(roles);
simpleAuthorizationInfo.setStringPermissions(permissions);
return simpleAuthorizationInfo;
}
/**
* 身份验证方法
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
User user = getUserByName(username);
if (user == null) {
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, user.getPassword(), "customRealm");
return simpleAuthenticationInfo;
}
private User getUserByName(String username) {
return userMapper.getUserByName(username);
}
private List<String> getPermissionsByRoles(List<String> roles) {
return permissionMapper.selectPermsByRoles(roles);
}
private List<String> getRoleByName(String username) {
List<String> roles = soleMapper.selectRoleByName(username);
return roles;
}
}
- 配置ShiroConfig
package com.zcx.demo.shiroweb.core.config;
import com.zcx.demo.shiroweb.shiro.filter.RoleRoFilter;
import com.zcx.demo.shiroweb.shiro.manager.CustomSessionManager;
import com.zcx.demo.shiroweb.shiro.realm.CustomRealm;
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.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
@Configuration
public class ShiroConfig {
@Bean
public CustomRealm customRealm(){
return new CustomRealm();
}
@Bean
public SecurityManager securityManager(CustomRealm customRealm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(customRealm);
return defaultWebSecurityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setUnauthorizedUrl("/500");
shiroFilterFactoryBean.setSuccessUrl("/index");
shiroFilterFactoryBean.setSecurityManager(securityManager);
HashMap<String, String> map = new HashMap<>();
map.put("/user/login","anon");
map.put("/user/testRole1","roles[admin]");
map.put("/user/testRole2","roles[admin,admin1]");
map.put("/user/testPerm1","perms[user:update]");
map.put("/user/testPerm2","perms[user:update,user:delete]");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
}
常用过滤器有:
authc:所有已登陆用户可访问
roles:有指定角色的用户可访问,通过[ ]指定具体角色,这里的角色名称与数据库中配置一致
perms:有指定权限的用户可访问,通过[ ]指定具体权限,这里的权限名称与数据库中配置一致
anon:所有用户可访问
- 在Controller中的使用
package com.zcx.demo.shiroweb.controller;
import com.zcx.demo.shiroweb.vo.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@ResponseBody
@RequestMapping(value = "login",method = RequestMethod.POST)
public String login(User user) {
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
token.setRememberMe(user.isRememberMe());
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
} catch (Exception e) {
return e.getMessage();
}
return "登录成功";
}
@RequiresRoles("admin")//除了在配置文件的过滤器中配置权限外还可以直接用注解设置权限
@RequiresPermissions({"user:update","user:delete"})
@RequestMapping("testRole1")
public String testRole1(){
return "admin test1 success";
}
@RequestMapping("/testRole2")
public String testRole2(){
return "admin test2 success";
}
@RequestMapping("/testPerm1")
public String testPerm1(){
return "Perm test1 success";
}
@RequestMapping("/testPerm2")
public String testPerm2(){
return "perm test2 success";
}
}
到这里Shiro简单的集成就完成了。
三.Shiro加密使用
上面只是Shiro的简单集成,没有配置Shiro的加密,但是有哪个公司的的密码没加密呢,加密后又怎么校验呢,现在就来讲加密校验这块,为了便于其他地方使用,建议抽出一个方法来做。
package com.zcx.demo.shiroweb.utils;
import com.zcx.demo.shiroweb.vo.User;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
public class EncryptHelp {
private RandomNumberGenerator numberGenerator = new SecureRandomNumberGenerator();
public static final String PASSWORD_ALGORITHM_NAME = "md5";
public static final int PASSWORD_HASH_ITERATIONS = 2;
public void encryptPassword(User user) {
user.setSalt(numberGenerator.nextBytes().toHex());
String newPassword = new SimpleHash(PASSWORD_ALGORITHM_NAME, user.getPassword(),
ByteSource.Util.bytes(user.getSalt()), PASSWORD_HASH_ITERATIONS).toHex();
user.setPassword(newPassword);
}
}
在ShiroConfig配置文件中添加加密,便于身份验证时调用:
@Configuration
public class ShiroConfig {
@Bean
public CredentialsMatcher credentialsMatcher(){
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(EncryptHelp.PASSWORD_ALGORITHM_NAME);
matcher.setHashIterations(EncryptHelp.PASSWORD_HASH_ITERATIONS);
return matcher;
}
@Bean
public CustomRealm customRealm(CredentialsMatcher credentialsMatcher){
CustomRealm customRealm = new CustomRealm();
customRealm.setCredentialsMatcher(credentialsMatcher);//设置加密方式
return customRealm;
}
然后再Realm的身份验证中将盐添加进去就好了:
public class CustomRealm extends AuthorizingRealm {
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
User user = getPasswordByName(username);
if (user == null) {
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, user.getPassword(), "customRealm");
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt()));//将盐设置到校验对象中
return simpleAuthenticationInfo;
}
}
这里加密就设置完了。
四.设置session共享
正常情况使用session是没啥问题的,但是当涉及到多台服务器时session就存在共享问题,现在普遍做法是通过redis来共享session。
- 编写RedisSessionDAO
import com.zcx.demo.shiroweb.utils.RedisUtil;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@Component
public class RedisSessionDAO extends AbstractSessionDAO {
private String SESSION_PREFIX = "session:";
@Autowired
private RedisUtil redisUtil;
private String getKey(String key) {
return SESSION_PREFIX + key;
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session,sessionId);
saveSession(session);
return sessionId;
}
private void saveSession(Session session) {
if (session == null || session.getId() == null){
return;
}
Serializable id = session.getId();
String key = getKey(id.toString());
redisUtil.set(key, session,600);
}
@Override
protected Session doReadSession(Serializable serializable) {
System.out.println("read session");
String key = getKey(serializable.toString());
return (Session) redisUtil.get(key);
}
@Override
public void update(Session session) throws UnknownSessionException {
saveSession(session);
}
@Override
public void delete(Session session) {
Serializable id = session.getId();
String key = getKey(id.toString());
redisUtil.delete(key);
}
@Override
public Collection<Session> getActiveSessions() {
Set<String> keys = redisUtil.keys(SESSION_PREFIX);
Set<Session> sessionSet = new HashSet<>(keys.size());
if (CollectionUtils.isEmpty(keys)){
return null;
}
for (String key : keys) {
Session value = (Session) redisUtil.get(key);
if (value == null) {
continue;
}
sessionSet.add(value);
}
return sessionSet;
}
}
- 在ShiroConfig中添加SessionManager
@Configuration
public class ShiroConfig {
@Bean
public SessionManager sessionManager(RedisSessionDAO sessionDao){
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager ();
defaultWebSessionManager.setSessionDAO(sessionDao);
return defaultWebSessionManager;
}
@Bean
public SecurityManager securityManager(CustomRealm customRealm, SessionManager sessionManager){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setSessionManager(sessionManager);
defaultWebSecurityManager.setRealm(customRealm);
return defaultWebSecurityManager;
}
}
这样session共享就设置好了,但是这样做每次获取session时都会去redis拿数据,对redis的开销还是蛮大的,所以一般还会做个缓存,那就是自定义SessionManager。
package com.zcx.demo.shiroweb.shiro.manager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import javax.servlet.ServletRequest;
import java.io.Serializable;
public class CustomSessionManager extends DefaultWebSessionManager {
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
if (sessionId == null) {
return null;
}
ServletRequest request = null;
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}
if (request != null) {
Object session = request.getAttribute(sessionId.toString());
if (session != null) {
return (Session) session;
}
}
Session session = super.retrieveSession(sessionKey);
if (request != null) {
request.setAttribute(sessionId.toString(), session);
}
return session;
}
}
将ShiroConfig中的DefaultWebSessionManager改为CustomSessionManager 就可以好了。
五.设置缓存
这里使用redis做为缓存。
- 实现Cache接口
package com.zcx.demo.shiroweb.shiro.cache;
import com.zcx.demo.shiroweb.utils.RedisUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.Set;
@Component
public class RedisCache<K,V> implements Cache<K,V> {
private static final String CACHE_PREFIX = "cache:";
@Autowired
private RedisUtil redisUtil;
private String getKey(String key) {
if (StringUtils.isEmpty(key)) {
return null;
}
return CACHE_PREFIX + key;
}
@Override
public V get(K o) throws CacheException {
if (o == null){
return null;
}
String key = getKey(o.toString());
return (V) redisUtil.get(key);
}
@Override
public V put(K k, V v) throws CacheException {
if (k == null || v == null){
return null;
}
String key = getKey(k.toString());
redisUtil.set(key,v);
return v;
}
@Override
public V remove(K k) throws CacheException {
if (k == null){
return null;
}
String key = getKey(k.toString());
V bytes = (V) redisUtil.get(key);
redisUtil.delete(key);
return bytes;
}
@Override
public void clear() throws CacheException {
}
@Override
public int size() {
return 0;
}
@Override
public Set<K> keys() {
return null;
}
@Override
public Collection<V> values() {
return null;
}
}
- 实现CacheManager接口
package com.zcx.demo.shiroweb.shiro.manager;
import com.zcx.demo.shiroweb.shiro.cache.RedisCache;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class RedisCacheManager implements CacheManager {
@Autowired
private RedisCache redisCache;
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return redisCache;
}
}
- 配置ShiroConfig
@Configuration
public class ShiroConfig {
@Bean
public SecurityManager securityManager(CustomRealm customRealm, SessionManager sessionManager, CacheManager cacheManager){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setSessionManager(sessionManager);
defaultWebSecurityManager.setCacheManager(cacheManager);
defaultWebSecurityManager.setRealm(customRealm);
return defaultWebSecurityManager;
}
}
六.设置保存cookie
- 配置ShiroConfig
@Configuration
public class ShiroConfig {
@Bean
public SecurityManager securityManager(CustomRealm customRealm, SessionManager sessionManager, CacheManager cacheManager
,CookieRememberMeManager rememberMeManager){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setSessionManager(sessionManager);
defaultWebSecurityManager.setCacheManager(cacheManager);
defaultWebSecurityManager.setRememberMeManager(rememberMeManager);
defaultWebSecurityManager.setRealm(customRealm);
return defaultWebSecurityManager;
}
@Bean
public CookieRememberMeManager cookieRememberMeManager() {
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
Cookie cookie = new SimpleCookie("rememberMe");
cookie.setMaxAge(1000000);
rememberMeManager.setCookie(cookie);
return rememberMeManager;
}
}
- Controller中使用
@RequestMapping("/user")
public class UserController {
@ResponseBody
@RequestMapping(value = "login",method = RequestMethod.POST)
public String login(User user) {
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
token.setRememberMe(user.isRememberMe());//设置记住登录
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
} catch (Exception e) {
return e.getMessage();
}
return "登录成功";
}
}
七.自定义过滤器
1.编写过滤器
package com.zcx.demo.shiroweb.shiro.filter;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class RoleRoFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
Subject subject = getSubject(servletRequest, servletResponse);
String[] roles = (String[]) o;
if (roles == null || roles.length == 0){
return true;
}
for (String role : roles) {
if (subject.hasRole(role)) {
return true;
}
}
return false;
}
}
- 添加到配置
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setLoginUrl("/login.html");
shiroFilterFactoryBean.setUnauthorizedUrl("/500.html");
shiroFilterFactoryBean.setSecurityManager(securityManager);
HashMap<String, String> map = new HashMap<>();
map.put("/user/login","anon");
map.put("/user/testRole1","roles[admin]");
map.put("/user/testRole2","roleOr[admin,admin1]");//使用过滤器
map.put("/user/testPerm1","perms[user:update]");
map.put("/user/testPerm2","perms[user:update,user:deletee]");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("roleOr",new RoleRoFilter());//添加过滤器
shiroFilterFactoryBean.setFilters(filterMap);
return shiroFilterFactoryBean;
}
}
最后附上demo地址:https://github.com/zcxshare/springboot-shiro-demo