spring boot和shiro的代码实战

  首先说明一下,这里不是基础教程,需要有一定的shiro知识,随便百度一下,都能找到很多的博客叫你基础,所以这里我只给出代码。

  官方文档:http://shiro.apache.org/spring-boot.html

  shiro的使用其实很简单的,配置也不麻烦,所以不要有任何畏惧。

正文开始

  项目结构:

   spring boot 和shiro的代码实战demo-LMLPHP

  pom.xml

  

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.hongcheng</groupId>
<artifactId>springboot-shiro-teset</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-shiro-teset</name>
<description>springboot的shiro项目demo</description> <properties>
<java.version>1.8</java.version>
</properties> <dependencies> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> <dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency> <dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies> <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build> </project>

  启动类:

package com.hongcheng.springboot_shiro_teset;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication
public class SpringbootShiroTesetApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootShiroTesetApplication.class, args);
}
}

  

  3个JavaBean

package com.hongcheng.springboot_shiro_teset.bean;

import java.util.Set;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 用户
* */
@Getter
@Setter
@ToString
@AllArgsConstructor
public class User {
private String id;
private String userName;
private String password;
/**
* 用户对应的角色集合
*/
private Set<Role> roles;
}
package com.hongcheng.springboot_shiro_teset.bean;

import java.util.Set;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString; /**
* 角色
* */
@Getter
@Setter
@ToString
@AllArgsConstructor
public class Role { private String id;
private String roleName;
/**
* 角色对应权限集合
*/
private Set<Permission> permissions;
}

  

  

package com.hongcheng.springboot_shiro_teset.bean;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 权限,shiro的权限,其实也只是一种资源的标识符,你拥有这个权限,就说明你有这个标识符,那你就可以访问这个资源
* */
@Getter
@Setter
@ToString
@AllArgsConstructor
public class Permission {
private String id;
private String permissionName;
}

  ShiroConfig

package com.hongcheng.springboot_shiro_teset.config;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map; import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
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.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate; import com.hongcheng.springboot_shiro_teset.dao.MySessionDao;
import com.hongcheng.springboot_shiro_teset.shiro.CustomRealm;
import com.hongcheng.springboot_shiro_teset.shiro.CustomRealm2; @Configuration
public class ShiroConfig { /**
* 启动代理 <br>
* 作用就是让controller中的@RequiresRoles("admin")、@RequiresPermissions("add")这些权限注解生效 <br>
* 如果不用注解权限,这里可以删掉 <br>
* */
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
} /**
* 加入注解的使用,不加入这个注解不生效 <br>
* 作用就是让controller中的@RequiresRoles("admin")、@RequiresPermissions("add")这些权限注解生效 <br>
* 如果不用注解权限,这里可以删掉 <br>
* */
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
} /**
* 将自己的验证方式加入容器 <br>
* Realm接口是shiro的一个重要接口,主要负责获取认证和授权信息
* */
@Bean
public CustomRealm myShiroRealm() {
CustomRealm customRealm = new CustomRealm();
/** 密码比较器,shiro有定义了不少hash类型的密码比较器,我们也可以自己重写个自定义的密码比较器,只需要
* 继承SimpleCredentialsMatcher这个类,同时重写doCredentialsMatch()这个方法就好了
* shiro会在org.apache.shiro.realm.AuthenticatingRealm.assertCredentialsMatch(AuthenticationToken, AuthenticationInfo)这个方法里面
* 调用这个密码比较器进行密码的比较
* */
// 如果不用自定义密码比较,md5CredentialsMatcher可以删掉
HashedCredentialsMatcher md5CredentialsMatcher = new HashedCredentialsMatcher();
md5CredentialsMatcher.setHashIterations(20);
md5CredentialsMatcher.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
customRealm.setCredentialsMatcher(md5CredentialsMatcher);
return customRealm;
} /**
* Realm可以有对多个,shiro可以根据不同的认证策略,搭配多个Realm进行认证 <br>
* 如果不用多realm验证,这里可以删掉 <br>
* */
@Bean
public CustomRealm2 myShiroRealm2() {
CustomRealm2 customRealm = new CustomRealm2();
return customRealm;
} /**
* 权限管理,配置主要是Realm的管理认证 <br>
* SecurityManager是shiro的一个重要接口,负责管理整个shiro的权限控制 <br>
* 所以我们需要给SecurityManager进行个性化配置
* */
@Bean
public SecurityManager securityManager(RedisTemplate<String, Object> redisTemplate) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
/***
* 多realm验证
* */
List<Realm> realmList = new LinkedList<Realm>();
realmList.add(myShiroRealm());
realmList.add(myShiroRealm2());
securityManager.setRealms(realmList);
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
/**
* 认证策略不管选择哪种,都会全部执行完,然后判断是否通过认证
* */
authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
authenticator.setRealms(realmList);
securityManager.setAuthenticator(authenticator);
/**
* session管理
* sessionManager如果不需要自定义,可以忽略,使用默认的
* */
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionIdUrlRewritingEnabled(true); // 允许sessionId进行url重写
/**
* shiro的session是会定期进行删除的
* 详情可以看这里:org.apache.shiro.session.mgt.AbstractValidatingSessionManager.enableSessionValidation()
* org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler.enableSessionValidation()
* */
sessionManager.setGlobalSessionTimeout(10000); // session超时时长
sessionManager.setDeleteInvalidSessions(true); // 删除失效session
sessionManager.setSessionValidationSchedulerEnabled(true); // 允许定期删除
sessionManager.setSessionValidationInterval(15000); // session定期删除时间
/**
* session保存进redis里
* sessionDao如果不需要,可以删掉
* */
MySessionDao sessionDao = new MySessionDao(redisTemplate);
sessionDao.setSessionIdGenerator(new JavaUuidSessionIdGenerator()); // sessionId生成器
sessionManager.setSessionDAO(sessionDao); securityManager.setSessionManager(sessionManager); return securityManager;
} /**
* Filter工厂,设置对应的过滤条件和跳转条件。shiro的权限控制,就是通过filter来控制的
详情看org.apache.shiro.web.filter.mgt.DefaultFilter Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
authcBearer org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter
* */
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
/**
* 请求资源的过滤
* 这一块是必须的,不能去掉
* */
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
//登出
map.put("/logout", "logout");
// 对login这个地址匿名访问
map.put("/login*", "anon");
map.put("/error*", "anon");
//对所有用户认证
map.put("/**", "authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login.html");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index.html");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error.html");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
}

  

  Realm

  

package com.hongcheng.springboot_shiro_teset.shiro;

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.authc.UnknownAccountException;
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 com.hongcheng.springboot_shiro_teset.bean.Permission;
import com.hongcheng.springboot_shiro_teset.bean.Role;
import com.hongcheng.springboot_shiro_teset.bean.User;
import com.hongcheng.springboot_shiro_teset.service.LoginService; public class CustomRealm extends AuthorizingRealm { /**
* 这里可以用@Autowired,因为CustomRealm是在ShiroConfig里面通过@Bean交由spring进行管理的
* */
@Autowired
private LoginService loginService; /**
* 授权
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取登录用户名
String name = (String) principals.getPrimaryPrincipal();
//根据用户名去数据库查询用户信息
User user = loginService.getUserByName(name);
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Role role : user.getRoles()) {
//添加角色
simpleAuthorizationInfo.addRole(role.getRoleName());
//添加权限
for (Permission permission : role.getPermissions()) {
simpleAuthorizationInfo.addStringPermission(permission.getPermissionName());
}
}
return simpleAuthorizationInfo;
} /***
* 认证,这里只是根据前端获取到的账号密码,让你去别的地方,如数据库,查找他的认证信息 <br>
* 密码的对比并不在这里,SimpleCredentialsMatcher的doCredentialsMatch()方法才是密码对比 <br>
* 如果用户不存在、或者用户被锁定,这里就应该抛出响应的错误<br>
* <br>
* <br>
* 如果使用Token令牌,controller处使用了EasyTypeToken这个类,记得自己去解析token,然后获取相应的用户信息
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//加这一步的目的是在Post请求的时候会先进认证,然后在到请求
if (authenticationToken.getPrincipal() == null) {
return null;
}
//获取用户信息
String name = authenticationToken.getPrincipal().toString();
User user = loginService.getUserByName(name);
if (user == null) {
//这里返回后会报出对应异常
throw new UnknownAccountException("账号不存在");
} else {
//这里验证authenticationToken和simpleAuthenticationInfo的信息
ByteSource salt = ByteSource.Util.bytes(name); // 加密用的盐
/**
* SimpleAuthenticationInfo这玩意就是你自己保存的用户认证信息,doGetAuthenticationInfo方法主要目的就是通过用户名、或者token去获取这个用户认证信息。
* 以便在后面给密码匹配器进行验证。
* 注意两个名词:
* principal:用户的主体、当事人,按官方的文档说法就是用户的唯一标识,不管你是账号,还是userId,还是身份证,只要唯一就行。
* credentials: 凭证,也就是密码,或者密钥,是可以证明用户身份的东西。
* */
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword().toString(), salt, getName());
return simpleAuthenticationInfo;
}
} }

  

package com.hongcheng.springboot_shiro_teset.shiro;

import org.apache.shiro.authc.AccountException;
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; /**
* 这个是用来多多realm验证的
* */
public class CustomRealm2 extends AuthorizingRealm {
/**
* 授权
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//添加角色
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addStringPermission("add");
return simpleAuthorizationInfo;
} /***
* 认证
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = authenticationToken.getPrincipal().toString();
if(!"123".equals(username)) {
throw new AccountException("账号找不到");
}
char[] password = (char[]) authenticationToken.getCredentials();
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,password,username);
return simpleAuthenticationInfo;
} }

  

  SessionDAO

package com.hongcheng.springboot_shiro_teset.dao;

import java.io.Serializable;
import java.util.Collection;
import java.util.List; import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.springframework.data.redis.core.RedisTemplate; /**
* 对session的一个持久化处理。<br>
* 将默认的ecache缓存换成了redis,可以作为一个分布式session共享实现
* */
public class MySessionDao extends EnterpriseCacheSessionDAO{ private RedisTemplate<String, Object> redisTemplate; /**
* 这里需要通过传值,将redisTemplate传入。 <br>
* 用@autowried注入是会不行的,因为MySessionDao <br>
* 是由我们自己new的,所以spring不会处理他的属性,另外,这里用applicationContent的getBean() <br>
* 也是会出现问题的,MySessionDao是在shiroConfig中的一个bean中创建的,他的创建顺序可能先于 <br>
* applicationContent的注入。
* */
public MySessionDao (RedisTemplate<String, Object> redisTemplate) {
setCacheManager(null);
this.redisTemplate = redisTemplate;
} /**
* 从redis里面取
* */
protected Session doReadSession(Serializable sessionId) {
System.err.println("获取Session" + sessionId);
Object object = redisTemplate.opsForHash().get("shiro-session", sessionId.toString());
return object == null?null:(Session)object;
} /**
* 更新redis
* */
protected void doUpdate(Session session) {
System.err.println("更新Session" + session.getId());
Serializable sessionId = session.getId();
redisTemplate.opsForHash().put("shiro-session", sessionId.toString(),session);
} /**
* 删除redis
* */
protected void doDelete(Session session) {
System.err.println("删除Session" + session.getId());
Serializable sessionId = session.getId();
redisTemplate.opsForHash().delete("shiro-session", sessionId.toString());
} /**
* 获取全部session,判断是否过期,如果过期,删除 <br>
* 默认实现:org.apache.shiro.session.mgt.eis.CachingSessionDAO.getActiveSessions() <br>
* 默认实现是从CacheManager得缓存里面取的,但是我们这里的没有设置CacheManager,所以一直为空 <br>
* 导致redis中已经过期了的session一直没有被清理,所以这里我们要重写
* */
@SuppressWarnings({ "unchecked", "rawtypes" })
public Collection<Session> getActiveSessions() {
List values = redisTemplate.opsForHash().values("shiro-session");
return values;
} }

  

RedisConfig

  

package com.hongcheng.springboot_shiro_teset.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration
public class RedisConfig {
/**
* redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
* 如果没有用的redis,那么这个类可以删掉
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置value的序列化规则和 key的序列化规则
/**
* 如果用GenericJackson2JsonRedisSerializer来序列号shiro的session,会出现反序列化失败的情况
* 可以考虑使用别的方法进行序列化,session应该要进行加密在保存
* 我这里为了方便,直接用jdk的序列化
* */
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(RedisSerializer.java());
redisTemplate.afterPropertiesSet();
return redisTemplate;
} }

LoginController

package com.hongcheng.springboot_shiro_teset.controller;

import javax.servlet.http.HttpSession;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import com.hongcheng.springboot_shiro_teset.bean.User; @RestController
public class LoginController { @RequestMapping("/login")
public String login(User user) { //添加用户认证信息
Subject subject = SecurityUtils.getSubject();
/**
* 如果是账号密码类型,可以创建一个UsernamePasswordToken。
* 如果使用Token令牌,可以使用EasyTypeToken这个类,相对于UsernamePasswordToken,只是把密码视为空串,username视为token。
* 如果用EasyTypeToken,记得要自己去解析token,然后获取相应的用户信息
* */
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
user.getUserName(),
user.getPassword()
);
try {
//进行验证,这里可以捕获异常,然后返回对应信息
subject.login(usernamePasswordToken);
// 判断有没有角色
subject.checkRole("admin");
// 判断有没有权限
subject.checkPermissions("query", "add");
} catch (AuthenticationException e) {
e.printStackTrace();
return "账号或密码错误!";
} catch (AuthorizationException e) {
e.printStackTrace();
return "没有权限";
}
return "login success";
}
//注解验角色和权限
@RequiresRoles("admin")
@RequiresPermissions("add")
@RequestMapping("/index")
public String index(HttpSession httpsession) {
Session session = SecurityUtils.getSubject().getSession();
System.err.println(session);
System.err.println(httpsession); /***
* shiro的session和request和response,其实就是HttpServletSession、HttpServletRequest、HttpServletResponse的子类
* org.apache.shiro.web.session.mgt.ServletContainerSessionManager.getSession(SessionKey)可以看到这个结论
* */
httpsession.setAttribute("http", "httpsession测试");
Object attribute = session.getAttribute("http");
System.err.println(attribute);
return "index!";
}
}

  

Service

package com.hongcheng.springboot_shiro_teset.service;

import com.hongcheng.springboot_shiro_teset.bean.User;

public interface LoginService {
User getUserByName(String getMapByName) ;
}

  

package com.hongcheng.springboot_shiro_teset.service;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set; import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.springframework.stereotype.Service; import com.hongcheng.springboot_shiro_teset.bean.Permission;
import com.hongcheng.springboot_shiro_teset.bean.Role;
import com.hongcheng.springboot_shiro_teset.bean.User; @Service
public class LoginServiceImpl implements LoginService {
@Override
public User getUserByName(String getMapByName) {
//模拟数据库查询,正常情况此处是从数据库或者缓存查询。
return getMapByName(getMapByName);
} /**
* 模拟数据库查询
* @param userName
* @return
*/
private User getMapByName(String userName){
//共添加两个用户,两个用户都是admin一个角色,
//chc有query和add权限,zhangsan只有一个query权限
Set<Permission> permissionsSet = new HashSet<>();
permissionsSet.add(new Permission("1","query"));
permissionsSet.add(new Permission("2","add")); Set<Role> roleSet = new HashSet<>();
roleSet.add(new Role("1","admin",permissionsSet)); User user = new User("1","chc",encrypt("123456","chc"),roleSet);
Map<String ,User> map = new HashMap<>();
map.put(user.getUserName(), user); Set<Permission> permissionsSet1 = new HashSet<>();
permissionsSet1.add(new Permission("3","query")); Set<Role> roleSet1 = new HashSet<>();
roleSet1.add(new Role("2","user",permissionsSet1)); User user1 = new User("2","zhangsan",encrypt("123456","zhangsan"),roleSet1);
map.put(user1.getUserName(), user1); return map.get(userName);
} /***
* 模拟加密
* */
private String encrypt(String password,String name) {
String algorithmName = Md5Hash.ALGORITHM_NAME; // 加密算法
String credentials = password; // 密码
String salt = name; // 加密盐值
int hashIterations = 20; // 加密次数
SimpleHash simpleHash = new SimpleHash(algorithmName, credentials, salt, hashIterations);
return simpleHash.toString();
} }

  

  application.properties

# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=10000

  

  login.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<form action="/login">
用户名:<input name = "userName">
<br>
密码:<input name = "password">
<button>提交</button>
</form>
</body>
</html>

  

  index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
成功登陆
<br>
<a href="logout">退出</a>
</body>
</html>

  error.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
错误
<br>
<a href="/login.html">登陆</a>
</body>
</html>

  

  MyExceptionHandler

package com.hongcheng.springboot_shiro_teset.filter;

import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody; import lombok.extern.slf4j.Slf4j; /**
* 对controller的一个切面增强,可以只针对部分controller
* 这个用不用都无所谓
* */
@ControllerAdvice(basePackages = "com.hongcheng.springboot_shiro_teset.controller")
@Slf4j
public class MyExceptionHandler { /**
* 统一异常处理
* */
@ExceptionHandler()
@ResponseBody
public String ErrorHandler(AuthorizationException e) {
log.error("没有通过权限验证!", e);
return "没有通过权限验证!";
}
}

  

启动访问localhost:8080,就可以自己看了

项目丢上码云了 https://gitee.com/1281003978/springboot_shiro_demo

2020/05/24,今天有空梳理下源码

一、登录校验:

1:前端输入账号密码,然后在我们的controller里面调用subject的login方法执行登录

spring boot 和shiro的代码实战demo-LMLPHP

 2:DelegatingSubject.class的login方法(DelegatingSubject是Subject的一个实现类):

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

3:紧着会进入DefaultSecurityManager的login方法,也就是我们的安全管理器

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

4:继续执行父类AuthenticatingSecurityManager的authenticate方法

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

注意:Authenticator就是一个认证器,专门用来获取认证的,默认是ModularRealmAuthenticator

spring boot 和shiro的代码实战demo-LMLPHP

5:进入AbstractAuthenticator的认证方法,就是我们上一步所提到的认证器

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

6:这里就是我们ModularRealmAuthenticator的实现方法了,根据你realm的个数,才用不同的方式进行认证

spring boot 和shiro的代码实战demo-LMLPHP

7:根据realm的个数,才用不同的方式进行认证

单个realm的时候很简单,直接用我们设置的realm进行获取认证信息就行

spring boot 和shiro的代码实战demo-LMLPHP

多个realm的时候比较复杂了,他会根据你设置的不同的策略来进行判断

spring boot 和shiro的代码实战demo-LMLPHP

 shiro有三种策略:

spring boot 和shiro的代码实战demo-LMLPHP

  • AllSuccessfulStrategy:全部realm都能根据传入的token获取到用户信息才算通过
  • AtLeastOneSuccessfulStrategy:至少有一个realm获取到用户信息就算通过,这种策略会把你全部的realm都执行一遍,哪怕你第一个realm就能获取到用户信息
  • FirstSuccessfulStrategy:只要有一个realm成功获取到用户信息,就不再继续执行其他realm了

8:你是不是以为已经到头了呢?其实还没有。还要再执行一下AuthenticatingRealm这个类的getAuthenticationInfo方法

AuthenticatingRealm这个类就是我们自定义realm需要继承的一个父类

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

9:这里就是我们的自定义realm拉,不过一定要注意,doGetAuthenticationInfo只是获取认证信息,也就是根据我们传入的账号 / 用户id / token,反正就是一个能让我们识别出来是哪个用户的东西,去获取这个用户的密码(凭证)

他并不是让你去比较密码是否一致

spring boot 和shiro的代码实战demo-LMLPHP

10:让我们重回第8步的方法,那里还有一个很重要的方法:密码对比

spring boot 和shiro的代码实战demo-LMLPHP

11:密码认证,认证不通过就会抛异常,如果是只有一个realm的,异常就直接抛了。不过如果是多realm的,就是在AuthenticationStrategy的实现类里面判断是不是要跑密码认证失败异常

spring boot 和shiro的代码实战demo-LMLPHP

shiro的一些密码匹配器,当然我们也可以自定义

spring boot 和shiro的代码实战demo-LMLPHP

12:继续回到我们最开始的controller,密码认证异常会一路往上抛,直到我们自己controller进行捕获然后返回信息。当然,如果没有异常那就是成功了

spring boot 和shiro的代码实战demo-LMLPHP

二、获取用户权限,也就是授权。一开始登录的时候,输入账号密码登录,这个时候其实只是认证信息而已,这个时候还没有进行获取权限,只有在shiro需要进行权限判断的时候才会去调用我们自己的realm来进行获取用户权限

1:我们直接让判断下这个用户有没有admin角色

spring boot 和shiro的代码实战demo-LMLPHP

2:调用DelegatingSubject的checkRole方法

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

3:调用AuthorizingSecurityManager的checkRole方法

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

有一点希望大家记得:我们平时所使用的SecurityManager都是继承了认证和授权功能的

spring boot 和shiro的代码实战demo-LMLPHP

4:调用ModularRealmAuthorizer的checkRole方法,和认证一样,没有权限就抛异常

spring boot 和shiro的代码实战demo-LMLPHP

5:调用ModularRealmAuthorizer的hasRole方法。为了授权,他会跑遍所有的realm

spring boot 和shiro的代码实战demo-LMLPHP

另外还有一点,看上图红框上面那句,如果我们的realm没有实现Authrizer接口,那就不会用我们的realm,所以如果我们要把认证和授权分开的话,就要分别实现。不然就直接继承AuthorizingRealm吧

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

6:调用AuthorizingRealm的hasRole方法

spring boot 和shiro的代码实战demo-LMLPHP

7:调用AuthorizingRealm的getAuthorizationInfo方法

spring boot 和shiro的代码实战demo-LMLPHP

8:又到了我们自己的realm了

spring boot 和shiro的代码实战demo-LMLPHP

9:又回到我们第6步的方法了,另外roles本身就是一个set集合

spring boot 和shiro的代码实战demo-LMLPHP

不信你看

spring boot 和shiro的代码实战demo-LMLPHP

三、我们不是配置过session过期策略么,那我们来看下shiro是怎么处理的:

在我们一开始获取subject的时候就会判断是不是要生成当时器去验证session,如果有必要,就会创建一个定时任务池去检查session

1:我们访问的时候,会生成一个subject对象

Subject接口里面有个静态内部类Builder

spring boot 和shiro的代码实战demo-LMLPHP

2:DefaultSecurityManager

spring boot 和shiro的代码实战demo-LMLPHP

 3:DefaultSecurityManager

spring boot 和shiro的代码实战demo-LMLPHP

4:DefaultSecurityManager

spring boot 和shiro的代码实战demo-LMLPHP

5:DefaultSecurityManager

spring boot 和shiro的代码实战demo-LMLPHP

6:DefaultSecurityManager

spring boot 和shiro的代码实战demo-LMLPHP

7:DefaultSecurityManager

spring boot 和shiro的代码实战demo-LMLPHP

8:DefaultSecurityManager

spring boot 和shiro的代码实战demo-LMLPHP

9:DefaultSecurityManager

spring boot 和shiro的代码实战demo-LMLPHP

10:DefaultSecurityManager

spring boot 和shiro的代码实战demo-LMLPHP

11:ExecutorServiceSessionValidationScheduler

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

ScheduledExecutorService service 是一个定时的线程池

外部调用ExecutorServiceSessionValidationScheduler的enableSessionValidation方法,让他把自己作为一个任务扔进任务池里,按指定时间重复执行

12:AbstractValidatingSessionManager

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

13:DefaultSessionManager

如果我们重写了sessionDAO,就必须要重写getActiveSessions方法,不然很多关于session的校验都会出问题

spring boot 和shiro的代码实战demo-LMLPHP

14:AbstractValidatingSessionManager

spring boot 和shiro的代码实战demo-LMLPHP

15:AbstractValidatingSessionManager

spring boot 和shiro的代码实战demo-LMLPHP

16:DefaultSessionManager

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

17:CachingSessionDAO

spring boot 和shiro的代码实战demo-LMLPHP

spring boot 和shiro的代码实战demo-LMLPHP

 18:MySessionDao

spring boot 和shiro的代码实战demo-LMLPHP

四:shiro 的 session、request、response和Tomcat为我们生成的session、request、response 有什么区别?

这里我先说一下结论。shiro有自己的session、request、response实现类,内部包装了Tomcat的session、request、response。

代码下次再补上

05-02 01:39