Springboot集成Shiro(前后端分离)
思维导图
什么是Shiro
shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。
spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。
shiro不依赖于spring,shiro不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。
使用shiro实现系统的权限管理,有效提高开发效率,从而降低开发成本。
用户通过subject登陆,形成一个UsernamePasswordToken,令牌,在域realm里完成认证、授权,成功后加入缓存。(realm可以写、也可以用默认的,也可以写很多个域)
Shiro架构
- subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
- securityManager:安全管理器,主体进行认证和授权都是通过securityManager进行。
- authenticator:认证器,主体进行认证最终通过authenticator进行的。
- authorizer:授权器,主体进行授权最终通过authorizer进行的。
- sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。
- SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
- cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
- realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。 在realm中存储授权和认证的逻辑。
认证过程
认证执行流程
1、通过ini配置文件创建securityManager
2、调用subject.login方法主体提交认证,提交的token
3、securityManager进行认证,securityManager最终由ModularRealmAuthenticator进行认证。
4、ModularRealmAuthenticator调用IniRealm(给realm传入token) 去ini配置文件中查询用户信息
5、IniRealm根据输入的token(UsernamePasswordToken)从 shiro.ini查询用户信息,根据账号查询用户信息(账号和密码)
代码语言:javascript复制如果查询到用户信息,就给ModularRealmAuthenticator返回用户信息(账号和密码)
如果查询不到,就给ModularRealmAuthenticator返回null
6、ModularRealmAuthenticator接收IniRealm返回Authentication认证信息
如果返回的认证信息是null,ModularRealmAuthenticator抛出异常(org.apache.shiro.authc.UnknownAccountException)
如果返回的认证信息不是null(说明inirealm找到了用户),对IniRealm返回用户密码 (在ini文件中存在) 和 token中的密码 进行对比,如果不一致抛出异常(org.apache.shiro.authc.IncorrectCredentialsException)
授权流程
1、对subject进行授权,调用方法isPermitted(”permission串”)
2、SecurityManager执行授权,通过ModularRealmAuthorizer执行授权
3、ModularRealmAuthorizer执行realm(自定义的Realm)从数据库查询权限数据
调用realm的授权方法:doGetAuthorizationInfo
4、realm从数据库查询权限数据,返回ModularRealmAuthorizer
5、ModularRealmAuthorizer调用PermissionResolver进行权限串比对
6、如果比对后,isPermitted中”permission串”在realm查询到权限数据中,说明用户访问permission串有权限,否则 没有权限,抛出异常。
实现
代码地址:https://github.com/cayzlh/spring-boot-shiro-demo
自定义Realm
CustRealm:
代码语言:javascript复制public class CustomRealm extends AuthorizingRealm {
@Autowired
@Lazy
private AuthorizingService authorizingService;
/**
* Shiro 的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();
* 当访问到页面的时候,链接配置了相应的权限或者 Shiro 标签才会执行此方法否则不会执行,
* 所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回 null 即可。
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) super.getAvailablePrincipal(principalCollection);
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
Set<String> roles = authorizingService.findRoleListByUsername(username);
authorizationInfo.setRoles(roles);
roles.forEach(role -> {
Set<String> permissions = authorizingService.findPermissionsByRole(roles);
authorizationInfo.addStringPermissions(permissions);
});
return authorizationInfo;
}
/**
* 在认证、授权内部实现机制中都有提到,最终处理都将交给Real进行处理。
* 因为在 Shiro 中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
* 通常情况下,在 Realm 中会直接从我们的数据源中获取 Shiro 需要的验证信息。
* 可以说,Realm 是专用于安全框架的 DAO. Shiro 的认证过程最终会交由 Realm 执行,
* 这时会调用 Realm 的getAuthenticationInfo(token)方法。
*
* 该方法主要执行以下操作:
*
* 1、检查提交的进行认证的令牌信息
* 2、根据令牌信息从数据源(通常为数据库)中获取用户信息
* 3、对用户信息进行匹配验证。
* 4、验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
* 5、验证失败则抛出AuthenticationException异常信息。
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
UserInfo user = authorizingService.selectByUserName(username);
if (null == user) {
throw new UnknownAccountException("doGetAuthenticationInfo() has an UnknownAccountException: " username);
}
String passwordInToken = new String(token.getPassword());
String passwordInDb = user.getPassword();
if (!StringUtils.equals(passwordInDb, passwordInToken)) {
throw new IncorrectCredentialsException("doGetAuthenticationInfo() has an IncorrectCredentialsException: " username);
}
return new SimpleAuthenticationInfo(username, passwordInToken, ByteSource.Util.bytes(user.getSalt()), getName());
}
}
自定义SessionManager
在我们项目中, 由于使用前后端分离的架构,所以要自定义Shiro的session管理:
代码语言:javascript复制public class CustomSessionManager extends DefaultWebSessionManager {
private static final String HEADER_TOKEN = "token";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public CustomSessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(HEADER_TOKEN);
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
return super.getSessionId(request, response);
}
}
}
通过监听请求Header里的token字段,如果有值,则作为shiro的sessionid。
配置Shiro
代码语言:javascript复制@Configuration
public class ShiroConfig {
@Bean(name = "customRealm")
public CustomRealm customRealm() {
return new CustomRealm();
}
@Bean(name = "sessionManager")
public SessionManager sessionManager() {
return new CustomSessionManager();
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
return shiroFilterFactoryBean;
}
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
其中:
代码语言:javascript复制@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
在spring boot中, shiro使用@RequiresRoles,@RequiresPermissions注解无效时,需要添加这两个配置。
其他代码
AuthorizingService:
代码语言:javascript复制@Service
public class AuthorizingService {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
@Autowired
private PermissionDao permissionDao;
public UserInfo selectByUserName(String username) {
return userDao.selectByUsername(username);
}
public Set<String> findRoleListByUsername(String username) {
return roleDao.selectByUsername(username);
}
public Set<String> findPermissionsByRole(Set<String> roles) {
HashSet<String> permissions = Sets.newHashSet();
roles.forEach(role -> permissions.addAll(permissionDao.selectByRole(role)));
return permissions;
}
public String login(String username, String password) {
UsernamePasswordToken token = new UsernamePasswordToken(username,
password);
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
currentUser.getSession().setTimeout(60 * 60 * 1000);
return currentUser.getSession().getId().toString();
}
public void logout() {
Subject currentUser = SecurityUtils.getSubject();
currentUser.logout();
}
}
PermissionDao:
代码语言:javascript复制@Repository
public class PermissionDao {
public Set<String> selectByRole(String role) {
switch (role) {
case "admin":
return Sets.newHashSet("Idea", "navicat", "notepad", "webstorm",
"chrome");
case "java":
return Sets.newHashSet("Idea");
case "mysql":
return Sets.newHashSet("navicat");
case "html":
return Sets.newHashSet("notepad");
case "javascript":
return Sets.newHashSet("webstorm");
case "guest":
return Sets.newHashSet("chrome");
default:
return Sets.newHashSet();
}
}
}
RoleDao:
代码语言:javascript复制@Repository
public class RoleDao {
public Set<String> selectByUsername(String username) {
switch (username) {
case "zhangsan":
return Sets.newHashSet("admin");
case "lisi":
return Sets.newHashSet("java", "mysql");
case "wangwu":
return Sets.newHashSet("html", "javascript");
default:
return Sets.newHashSet("guest");
}
}
}
UserDao:
代码语言:javascript复制@Repository
public class UserDao {
public UserInfo selectByUsername(String username) {
switch (username) {
case "zhangsan":
return UserInfo.builder()
.userName("zhangsan").password("123456").salt("123456")
.build();
case "lisi":
return UserInfo.builder()
.userName("lisi").password("123456").salt("123456")
.build();
case "wangwu":
return UserInfo.builder()
.userName("wangwu").password("123456").salt("123456")
.build();
default:
return null;
}
}
}
这里没有使用数据库, 直接模拟数据库操作。
集群
在实际项目运行中,为了达到高可用的目的,通常要把应用部署在多台服务器上,这个时候就要对session进行集群的管理
添加redis支持
pom.xml
代码语言:javascript复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
配置文件
代码语言:javascript复制spring:
application:
name: shiro-demo
redis:
host: redistest.xxxx.com
port: 6379
database: 10
password: test@2018
timeout: 180000
jedis:
pool:
max-active: 100
max-wait: 360000
min-idle: 0
max-idle: 100
my:
shiro:
session:
expireTime: 1800
prefix: you-shiro-session
新增Java类:ShiroCacheManager
代码语言:javascript复制public class ShiroCacheManager implements CacheManager {
private RedisTemplate redisTemplate;
private int expireTime;
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
return new ShiroRedisCache<>(name, redisTemplate, expireTime);
}
public ShiroCacheManager(RedisTemplate redisTemplate, int expireTime) {
this.redisTemplate = redisTemplate;
this.expireTime = expireTime;
}
public class ShiroRedisCache<K, V> implements Cache<K, V> {
private String cacheKey;
private RedisTemplate redisTemplate;
private int expireTime;
private ShiroRedisCache(String cacheKey, RedisTemplate redisTemplate, int expireTime) {
this.cacheKey = cacheKey;
this.redisTemplate = redisTemplate;
this.expireTime = expireTime;
}
private Object hashKey(K key) {
if (key instanceof PrincipalCollection) {
PrincipalCollection principalCollection = (PrincipalCollection) key;
return principalCollection.getPrimaryPrincipal().toString();
}
return key;
}
@Override
public V get(K key) throws CacheException {
BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
Object realKey = hashKey(key);
return boundHashOperations.get(realKey);
}
@Override
public V put(K key, V value) throws CacheException {
BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
Object realKey = hashKey(key);
boundHashOperations.put((K) realKey, value);
boundHashOperations.expire(expireTime, TimeUnit.SECONDS);
return value;
}
@Override
public V remove(K key) throws CacheException {
BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
Object realKey = hashKey(key);
V value = boundHashOperations.get(realKey);
boundHashOperations.delete(realKey);
return value;
}
@Override
public void clear() throws CacheException {
redisTemplate.delete(cacheKey);
}
@Override
public int size() {
BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
return boundHashOperations.size().intValue();
}
@Override
public Set<K> keys() {
BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
return boundHashOperations.keys();
}
@Override
public Collection<V> values() {
BoundHashOperations<String, K, V> boundHashOperations = redisTemplate.boundHashOps(cacheKey);
return boundHashOperations.values();
}
}
}
新增java类:RedisSessionDAO,继承EnterpriseCacheSessionDAO
代码语言:javascript复制public class RedisSessionDAO extends EnterpriseCacheSessionDAO {
private int expireTime;
private String prefix;
private RedisTemplate redisTemplate;
public RedisSessionDAO(RedisTemplate redisTemplate, int expireTime, String prefix) {
this.redisTemplate = redisTemplate;
this.expireTime = expireTime;
this.prefix = prefix;
}
@Override
protected Serializable doCreate(Session session) {
log.info("doCreate({})", session.getId());
Serializable sessionId = super.doCreate(session);
redisTemplate.opsForValue().set(prefix ":" sessionId.toString(), session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
log.info("doReadSession({})", sessionId);
Session session = super.doReadSession(sessionId);
if (session == null) {
session = (Session) redisTemplate.opsForValue().get(prefix ":" sessionId.toString());
}
return session;
}
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
String key = prefix ":" session.getId().toString();
if (!redisTemplate.hasKey(key)) {
redisTemplate.opsForValue().set(key, session);
}
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
}
@Override
protected void doDelete(Session session) {
log.info("doDelete({})", session.getId());
super.doDelete(session);
redisTemplate.delete(prefix ":" session.getId().toString());
}
}
新增Java类:RedisObjectSerializer implements RedisSerializer
代码语言:javascript复制public class RedisObjectSerializer implements RedisSerializer<Object> {
private Converter<Object, byte[]> serializer = new SerializingConverter();
private Converter<byte[], Object> deserializer = new DeserializingConverter();
static final byte[] EMPTY_ARRAY = new byte[0];
@Override
public Object deserialize(byte[] bytes) {
if (isEmpty(bytes)) {
return null;
}
try {
return deserializer.convert(bytes);
} catch (Exception ex) {
throw new SerializationException("Cannot deserialize", ex);
}
}
@Override
public byte[] serialize(Object object) {
if (object == null) {
return EMPTY_ARRAY;
}
try {
return serializer.convert(object);
} catch (Exception ex) {
return EMPTY_ARRAY;
}
}
private boolean isEmpty(byte[] data) {
return (data == null || data.length == 0);
}
}
修改ShiroConfig
代码语言:javascript复制@Configuration
public class ShiroConfig implements InitializingBean {
@Value("${my.shiro.session.expireTime:1800}")
private int expireTime;
@Value("${my.shiro.session.prefix:you-shiro-session}")
private String prefix;
@Autowired
private RedisTemplate redisTemplate;
@Override
public void afterPropertiesSet() {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new RedisObjectSerializer());
redisTemplate.afterPropertiesSet();
}
@Bean
public RedisTemplate redisTemplate() {
return new RedisTemplate();
}
@Bean(name = "shiroCacheManager")
public ShiroCacheManager shiroCacheManager() {
return new ShiroCacheManager(redisTemplate, expireTime);
}
@Bean(name = "redisSessionDAO")
public RedisSessionDAO redisSessionDAO() {
return new RedisSessionDAO(redisTemplate, expireTime, prefix);
}
@Bean(name = "customRealm")
public CustomRealm customRealm() {
return new CustomRealm();
}
@Bean(name = "sessionManager")
public SessionManager sessionManager() {
CustomSessionManager sessionManager = new CustomSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
securityManager.setCacheManager(shiroCacheManager());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
return shiroFilterFactoryBean;
}
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
集群完成
^_^.
经过以上改造,shiro就可以在分布式应用中集群使用。
项目代码,在 with-redis 分支中。
测试
运行Demo,测试登录和请求其他接口:
–
分享计划
博客内容将同步至腾讯云 社区,邀请大家一同入驻:https://cloud.tencent.com/
许可协议
本文采用 署名-非商业性使用-相同方式共享 4.0 国际 许可协议,转载请注明出处。