技术栈:SpringBoot+Shiro
代码实现
pom文件加坐标
Springboot版本选择2.7.14 ;java版本1.8 ; shiro做了版本锁定 1.3.2
<properties>
<java.version>1.8java.version>
<shiro.version>1.3.2shiro.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.16.16version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.21version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.3version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>${shiro.version}version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>${shiro.version}version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-webartifactId>
<version>${shiro.version}version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-ehcacheartifactId>
<version>${shiro.version}version>
dependency>
dependencies>
主配置文件
#配置数据源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring?serverTimezone=GMT
username: root
password: 123456
#配置自动驼峰映射
mybatis:
configuration:
map-underscore-to-camel-case: true
type-aliases-package: com.dong.pojo
#MP配置自动驼峰映射
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #mybatis所执行的sql输出控制台
POJO实体类
Permission
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
@TableName(value = "pe_permission")
public class Permission {
@TableField(value = "id")
private String id;
@TableField(value = "name")
private String name;
@TableField(value = "code")
private String code;
@TableField(value = "description")
private String description;
}
Role
@NoArgsConstructor
@AllArgsConstructor
@Data
@TableName(value = "pe_role")
@Component
public class Role {
@TableField(value = "id")
private String id;
@TableField(value = "name")
private String name;
@TableField(value = "code")
private String code;
@TableField(value = "description")
private String description;
// 外部属性
@TableField(exist = false)
private List<Permission> permissions;
}
Users
@NoArgsConstructor
@AllArgsConstructor
@Data
@TableName(value = "pe_user")
@Component
public class Users {
@TableId(value = "id")
private String id;
@TableField(value = "username")
private String username;
@TableField(value = "password")
private String password;
@TableField(value = "salt")
private String salt;
// 外部属性
@TableField(exist = false)
private List<Role> rolesList;
public Users(String id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
}
dao层
UsersMapper
@Mapper
public interface UserMapper extends BaseMapper<Users> {
@Insert("insert into pe_user(id,username,password,salt) values(#{id},#{username},#{password},#{salt})")
public int save(Users users);
// 级联查询
@Results(id = "users",value = {
@Result(column = "id",property = "rolesList",many = @Many(select = "com.dong.springboot_mp_shiro.com.dong.mapper.RoleMapper.findById"))
})
@Select("select * from pe_user where username=#{v}")
public Users findUserDetail(String name);
// 简单查询
@Select("select * from pe_user where username=#{v}")
public Users findBaseUser(String name);
}
RoleMapper
@Mapper
public interface RoleMapper extends BaseMapper<Role> {
@Results(id = "role",value = {
@Result(column = "id",property = "permissions",many=@Many(select = "com.dong.springboot_mp_shiro.com.dong.mapper.PermissionMapper.findByPermissionId"))
})
@Select("select * from pe_role where id in (select role_id from pe_user_role where user_id =#{v} )")
public Role findById(String id);
}
PermissionMapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dong.springboot_mp_shiro.com.dong.pojo.Permission;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface PermissionMapper extends BaseMapper<Permission> {
@Select("select * from pe_permission where id in (select permission_id from pe_role_permission where role_id =#{v})")
public Permission findByPermissionId(String permissionId);
}
service层
接口:
IUserService
public interface IUserService {
public int save(Users users);
public Users baseFindUser(String name);
public Users findUserDetail(String Name);
}
IRoleService
public interface IRoleService {
}
IPermissionService
public interface IPermissionService {
}
实现类:
UserServiceImp
@Service
public class UserServiceImp implements IUserService {
@Autowired(required = false)
private UserMapper uMapper;
@Override
public int save(Users users) {
System.out.println("service:"+ users);
// 获取salt字符串
String salt = DigestsUtil.generateSalt();
// 密码加密
String password = DigestsUtil.generatePassword(users.getPassword(), salt);
users.setPassword(password);
users.setSalt(salt);
int res = uMapper.save(users);
return res;
}
@Override
public Users baseFindUser(String name) {
Users baseUser = uMapper.findBaseUser(name);
return baseUser;
}
@Override
public Users findUserDetail(String name) {
Users userDetail = uMapper.findUserDetail(name);
return userDetail;
}
}
controller层
@RestController
public class UserController {
@Autowired(required = false)
private UserServiceImp service;
// 首页
@RequiresPermissions("user-home")
@RequestMapping("/user/home")
public String home(){
return "访问个人主页成功";
}
// 用户注册
@RequiresPermissions("user-add")
@RequestMapping("/user/{id}")
public String save(@PathVariable String id){
/*int res = service.save(users);
if(res>0){
return "添加成功";
}else{
return "添加失败";
}*/
return "新增成功";
}
@RequiresPermissions("user-delete")
@RequestMapping(value = "/user/{id}",method = RequestMethod.DELETE)
public String delete(@PathVariable String id){
return "删除成功";
}
@RequiresPermissions("user-update")
@RequestMapping(value = "/user/{id}",method = RequestMethod.PUT)
public String update(@PathVariable String id){
return "修改成功";
}
@RequiresPermissions("user-find")
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String find(){
return "查询成功";
}
// 登录认证
@RequestMapping("/login")
public String login(Users users){
try {
// 构造登录令牌
UsernamePasswordToken token = new UsernamePasswordToken(users.getUsername(), users.getPassword());
// 获取subject
Subject subject = SecurityUtils.getSubject();
// 调用subject认证
subject.login(token);
return "登录成功";
} catch (AuthenticationException e) {
return "用户名或密码错误";
}
}
// 未登录跳转
@RequestMapping("/autherror")
public String autherror(){
return "未认证,请登录";
}
}
==@RequiresPermissions(" "):==标注访问该资源需要的权限
执行subject.long登录方法,执行Reaml的AuthenticationException方法
鉴权授权,执行Reaml的AuthorizationInfo方法
MyRealm
public class MyRealm extends AuthorizingRealm {
@Autowired(required = false)
private UserServiceImp serviceImp;
// 授权鉴权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取已经认证的用户数据
Users users = (Users) principalCollection.getPrimaryPrincipal();
// 查询用户的详细信息
Users userDetail = serviceImp.findUserDetail(users.getUsername());
HashSet<String> perms = new HashSet<>(); // 权限set集合
HashSet<String> roles = new HashSet<>(); // 角色set集合
for(Role role: userDetail.getRolesList() ){
roles.add(role.getCode());
for(Permission permission: role.getPermissions() ){
perms.add(permission.getCode());
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(perms);
info.setRoles(roles);
return info;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户登陆输入的密码(token)
UsernamePasswordToken upToken = (UsernamePasswordToken)authenticationToken;
// 用户输入的账号
String username = upToken.getUsername();
Users users = serviceImp.baseFindUser(username);
if(users != null){
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(users, users.getPassword(), ByteSource.Util.bytes(users.getSalt()), "MyRealm");
return info;
}
// 账号查不到,返回null(抛出异常)
return null;
}
@PostConstruct // 属性初始化
public void initCredentialsMatcher(){
// 指定密码算法
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(DigestsUtil.SHA1);
// 指定迭代次数
hashedCredentialsMatcher.setHashIterations(DigestsUtil.COUNTS);
// 生成密码比较器
setCredentialsMatcher(hashedCredentialsMatcher);
}
}
@PostConstruct注解,属性初始化
加密工具类
public class DigestsUtil {
// 编码方式
public static final String SHA1="SHA-1";
// 加密次数
public static final Integer COUNTS=369;
// 获取salt字符串
public static String generateSalt(){
SecureRandomNumberGenerator secureRandomNumberGenerator = new SecureRandomNumberGenerator();
return secureRandomNumberGenerator.nextBytes().toHex();
}
// 生成密文密码
public static String generatePassword(String input,String salt){
return new SimpleHash(SHA1,input,salt,COUNTS).toString();
}
}
Shiro配置类:ShiroConfiguration
@Configuration
public class ShiroConfiguration {
/**
* 1.创建shiro自带cookie对象
*/
@Bean
public SimpleCookie sessionIdCookie(){
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName("ShiroSession");
return simpleCookie;
}
//2.创建realm
@Bean
public MyRealm getRealm() {
return new MyRealm(); //new 自定义的Reaml
}
/**
* 3.创建会话管理器
*/
@Bean
public DefaultWebSessionManager sessionManager(){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionValidationSchedulerEnabled(false);
sessionManager.setSessionIdCookieEnabled(true);
sessionManager.setSessionIdCookie(sessionIdCookie());
sessionManager.setGlobalSessionTimeout(3600000);
return sessionManager;
}
//4.创建安全管理器
@Bean
public SecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(getRealm());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 5.保证实现了Shiro内部lifecycle函数的bean执行
*/
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 6.开启对shiro注解的支持
* AOP式方法级权限检查
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 7.配合DefaultAdvisorAutoProxyCreator事项注解权限校验
*/
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(defaultWebSecurityManager());
return authorizationAttributeSourceAdvisor;
}
/*
* 8.配置shiro的过滤器工厂再web程序中,shiro进行权限控制全部是通过一组过滤器集合进行控制
* */
@Bean
public ShiroFilterFactoryBean shiroFilter(){
// 1.创建过滤工厂
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
// 2.设置安全管理器
filterFactoryBean.setSecurityManager(defaultWebSecurityManager());
// 3.通用配置(跳转登录页面,为授权跳转的页面)
filterFactoryBean.setLoginUrl("/autherror");
//4.设置过滤器集合
//key = 拦截的url地址
//value = 过滤器类型
LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/login","anon");//当前请求地址可以匿名访问
filterMap.put("/user/**","authc");// 当前请求地址必须认证之后才可以访问
// 在过滤器工程内设置系统过滤器
filterFactoryBean.setFilterChainDefinitionMap(filterMap);
return filterFactoryBean;
}
}
统一异常处理器
@ControllerAdvice
public class UserControllerAdv {
@ExceptionHandler(value = AuthorizationException.class)
@ResponseBody
public String tongyi(HttpServletRequest request , HttpServletResponse response, AuthorizationException e){
return "未授权---统一异常处理器";
}
}
代码结构:代码结构和SpringBoot整合Shiro中的案例相同
问题:如图
解决思路:将当前的session会话存到缓存Redis中
实现步骤:
代码实现:
RedisSessionDao
public class RedisSessionDao extends AbstractSessionDAO {
@Autowired
private RedisTemplate redisTemplate;
//创建会话
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
redisTemplate.opsForValue().set(sessionId,session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
return (Session) redisTemplate.opsForValue().get(sessionId);
}
@Override
public void delete(Session session) {
redisTemplate.delete(session.getId());
}
@Override
public Collection<Session> getActiveSessions() {
return Collections.emptySet();
}
@Override
public void update(Session session) {
redisTemplate.opsForValue().set(session.getId(),session);
}
}
需要操作Redis,整合Redis
导入redis坐标
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
yaml主配置文件配置redis
server:
port: 8080
#配置数据源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring?serverTimezone=GMT
username: root
password: 123456
redis:
port: 6379
#配置自动驼峰映射
mybatis:
configuration:
map-underscore-to-camel-case: true
type-aliases-package: com.dong.pojo
#MP配置自动驼峰映射
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #mybatis所执行的sql输出控制台
ShiroConfig
向容器中注入一个SessionDao,把SessionDao绑定给会话管理器
@Configuration
public class ShiroConfiguration {
/**
* 1.创建shiro自带cookie对象
*/
@Bean
public SimpleCookie sessionIdCookie(){
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName("ShiroSession");
return simpleCookie;
}
//2.创建realm
@Bean
public MyRealm getRealm() {
return new MyRealm();
}
// 向容器中注入一个SessionDao
@Bean
public SessionDAO redisSessionDao(){
RedisSessionDao sessionDAO = new RedisSessionDao();
return sessionDAO;
}
/**
* 3.创建会话管理器
*/
@Bean
public DefaultWebSessionManager sessionManager(){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//把SessionDao绑定给会话管理器
sessionManager.setSessionDAO(redisSessionDao());
sessionManager.setSessionValidationSchedulerEnabled(false);
sessionManager.setSessionIdCookieEnabled(true);
sessionManager.setSessionIdCookie(sessionIdCookie());
sessionManager.setGlobalSessionTimeout(3600000);
return sessionManager;
}
//4.创建安全管理器
@Bean
public SecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(getRealm());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 5.保证实现了Shiro内部lifecycle函数的bean执行
*/
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 6.开启对shiro注解的支持
* AOP式方法级权限检查
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 7.配合DefaultAdvisorAutoProxyCreator事项注解权限校验
*/
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(defaultWebSecurityManager());
return authorizationAttributeSourceAdvisor;
}
/*
* 8.配置shiro的过滤器工厂再web程序中,shiro进行权限控制全部是通过一组过滤器集合进行控制
* */
@Bean
public ShiroFilterFactoryBean shiroFilter(){
// 1.创建过滤工厂
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
// 2.设置安全管理器
filterFactoryBean.setSecurityManager(defaultWebSecurityManager());
// 3.通用配置(跳转登录页面,为授权跳转的页面)
filterFactoryBean.setLoginUrl("/autherror");
//4.设置过滤器集合
//key = 拦截的url地址
//value = 过滤器类型
LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/login","anon");//当前请求地址可以匿名访问
filterMap.put("/user/**","authc");
filterFactoryBean.setFilterChainDefinitionMap(filterMap);
return filterFactoryBean;
}
}