注意坑 springboot项目中如果有 aop的依赖时候,shiro的注解会失效
需要在配置类中,弄一个bean
@Bean public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator(); /** * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。 * 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。 * 加入这项配置能解决这个bug */ defaultAdvisorAutoProxyCreator.setUsePrefix(true); return defaultAdvisorAutoProxyCreator; }
1.安全框架的shiro核心功能
Authentication: 认证 判断是否为合法用户 有时被称为 "登录”,这是证明用户合法
Authorization: 授权 前提:认证过了; 判断你是否有权访问某个资源
Session Management: session管理
Cryptography: 加解密
(支持功能,核心功能就上面4个)
Web Support: 支持 web
Caching: 支持缓存
Concurrency: 支持并发
Testing: 支持测试
"Run As": 用其他用户登录
"Remember Me": 记住我
简记
authc : Authentication 认证
authz: Authorization 授权
授权基于角色实现(数据库的表)
role 角色 (表)
permission 权限 (表)
user 用户 (表)
都是多对多关系,所以还需要建立他们之间的关系表
user-role 用户角色关系表
role-permission 角色权限关系表
1 、前期工作:
数据库建表 :用户表 ,角色表,权限表,角色权限关系表,用户角色关系表。。并且插入一些模拟数据
create table shiro_user ( user_id int primary key auto_increment comment '用户ID', user_name varchar(256) not null comment '用户名', user_password varchar(256) not null comment '用户密码', status varchar(64) not null comment '账户状态 Nomal|locked|Cansel' ) comment ='用户表'; create table shiro_role ( role_id int primary key auto_increment comment '角色ID', role_name varchar(256) not null comment '角色名' ) comment ='角色表'; create table shiro_user_role ( user_role_id int primary key auto_increment comment '关系表ID', role_id int not null comment '角色ID', user_id int not null comment '用户ID' ) comment ='用户角色关系表'; create table shiro_permission ( permission_id int primary key auto_increment comment '权限ID', permission_name varchar(256) not null comment '权限名' ) comment ='权限表'; create table shiro_permission_role ( permission_role_id int primary key auto_increment comment '关系表ID', role_id int not null comment '角色ID', permission_id int not null comment '权限ID' ) comment ='权限角色关系表'; # 插入用户 insert into shiro_user VALUES (default,'tom','tom11','NOMAL'); insert into shiro_user VALUES (default,'jery','jery11','NOMAL'); insert into shiro_user VALUES (default,'victor','victor11','NOMAL'); insert into shiro_user VALUES (default,'kangkang','kangkang11','NOMAL'); insert into shiro_user VALUES (default,'xiaomei','xiaomei11','NOMAL'); # 插入角色 insert into shiro_role VALUES (default,'student'); insert into shiro_role VALUES (default,'teacher'); # 插入角色权限 insert into shiro_permission VALUES (default,'doHomeWork'); insert into shiro_permission VALUES (default,'talking'); insert into shiro_permission VALUES (default,'say'); insert into shiro_permission VALUES (default,'running'); insert into shiro_permission VALUES (default,'teacher'); insert into shiro_permission VALUES (default,'fireStudent'); insert into shiro_permission VALUES (default,'downStudent'); # 给用户绑定角色 insert into shiro_user_role VALUES (default,1,1); insert into shiro_user_role VALUES (default,1,2); insert into shiro_user_role VALUES (default,1,3); insert into shiro_user_role VALUES (default,2,4); insert into shiro_user_role VALUES (default,2,5); # 给角色绑定权限 insert into shiro_permission_role VALUES (default,1,1); insert into shiro_permission_role VALUES (default,1,2); insert into shiro_permission_role VALUES (default,1,3); insert into shiro_permission_role VALUES (default,1,4); insert into shiro_permission_role VALUES (default,2,5); insert into shiro_permission_role VALUES (default,2,6); insert into shiro_permission_role VALUES (default,2,7);
建立springboot-mybatis的web项目(选择spring Web依赖,lombok,MySQL Driver,Mybatis plus Framework,)
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</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> <!--mybatisplus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <!--数据库的--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--shiro安全框架的依赖--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.4.1</version> </dependency> <!--swagger 自动开发文档依赖--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <!--代码生成器依赖--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.1</version> <scope>test</scope> </dependency> <!--事务控制的依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!--hutool依赖--> <!--字符串检查,雪花算法生成订单号,复制对象实体类,等等工具--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.5</version> </dependency> <!--aop依赖,切面编程用的(日志切面使用) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!--数据校验使用的--> <!--数据校验依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
package org.victor.shirodemo_01; import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.config.rules.DateType; import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.victor.shirodemo_01.common.Log; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Scanner; /** * @program: test_paper_project * @ClassName CreatIngAuotCode * @description: * @author: victorGang * @create: 2022-09-03 10:23 * @Version 1.0 **/ @Slf4j public class CreatIngAuotCode { // 放在测试目录里面!!! //表所在的数据库信息,密码 public static final String JDBC_URL = "jdbc:mysql://127.0.0.1:3306/_0913shiro_01?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8"; public static final String JDBC_USER = "root"; public static final String JDBC_PASSWORD = "123456"; // 作者 写自己名字就行 public static final String AUTHOR = "VictorGang"; //项目的根目录的!!上一级,上一级!! 复制根目录后把最后一个层级掐掉 public static final String PARENT_PACKAGE = "org.victor"; // 项目的目录__最后一级目录 上面掐掉的 那个级 单独放这里 public static final String MODULE_NAME = "shirodemo_01"; //表的抬头,数据库里面的 设计数据库表 以项目名 为前缀 public static final String TABLE_PREFIX = "shiro_"; @Log("根据数据库表自动生成一系列的类") public static void main(String[] args) { String projectPath = System.getProperty("user.dir") + "/src/main/"; String javaPath = projectPath + "/java"; //自动生成类的存放路径 String resourcePath = projectPath + "/resources/mappers"; //自动生成mapper.xml的存放路径 FastAutoGenerator.create(JDBC_URL, JDBC_USER, JDBC_PASSWORD) .globalConfig(builder -> { builder.author(AUTHOR)// 设置作者 .dateType(DateType.ONLY_DATE) .disableOpenDir()//不打开文件夹 .enableSwagger()// 自动集成 那个swagger 但是不全,需要自己再添加一些,如果要搞这个,必须导入依赖 .outputDir(javaPath) .fileOverride()//覆盖文件 如果之前有就把之前的覆盖掉, ; // 指定输出目录 }) .packageConfig(builder -> { builder.parent(PARENT_PACKAGE) // 设置父包名 .moduleName(MODULE_NAME) // 设置父包模块名 .entity("pojo")//设置实体类包名 .mapper("mapper")//设置mapper包名 .service("service") .serviceImpl("service.impl") .pathInfo(Collections.singletonMap(OutputFile.xml, resourcePath)); // 设置mapperXml生成路径 }) .strategyConfig((scanner,builder) -> { builder.addInclude(getTables(scanner.apply("请输入表名,多个英文逗号分隔?所有输入 all"))) // 设置需要生成的表名 .addTablePrefix(TABLE_PREFIX) .serviceBuilder() .formatServiceFileName("%sService") //去掉service的I,不搞这个会以service接口I开头 .controllerBuilder() .enableRestStyle() //restful开启 // .entityBuilder().enableLombok() //开启lombok ; // 设置过滤表前缀 //设置生成的controller层的 注解和一些前缀 }) .templateEngine(new VelocityTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板 .execute(); log.info("执行完毕"); } public static List<String> scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ",用英文逗号分隔:"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotEmpty(ipt)) { return Arrays.asList(StringUtils.split(ipt, ",")); } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } // 处理 all 情况,把这个数据库所有的表,都一次性生成代码 protected static List<String> getTables(String tables) { return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(",")); } }
1 yml的配置
#端口的配置 server: port: 8081 #数据库的配置 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/_0913shiro_01?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8 password: 123456 username: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mvc: format: date: yyyy-MM-dd HH:mm:ss #mybatisPuls的配置 mybatis-plus: mapper-locations: "classpath:mappers/*.xml" configuration: log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl map-underscore-to-camel-case: true type-aliases-package: org.victor.shirodemo_01.pojo #shiro的配置 shiro: loginUrl: "/login.html" #日志配置 logging: pattern: level: org.victor.shirodemo_01: info
2 配置类
package org.victor.shirodemo_01.common; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition; import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.victor.shirodemo_01.pojo.ShiroRealm; /** * @program: shirodemo_01 * @ClassName SecurityConfig * @description: * @author: victorGang * @create: 2022-09-14 15:12 * @Version 1.0 **/ @Configuration public class SecurityConfig { //安全框架的realm 提供数据源的,用来存储,从数据库获取的对象,,自己创建的 @Bean public Realm shiroRealm(){ return new ShiroRealm(); } // shiro的过滤链 @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition(){ DefaultShiroFilterChainDefinition sfcd=new DefaultShiroFilterChainDefinition(); //anon表示直接放过 sfcd.addPathDefinition("/","anon"); sfcd.addPathDefinition("/login","anon"); sfcd.addPathDefinition("/login.html","anon"); sfcd.addPathDefinition("/css","anon"); sfcd.addPathDefinition("/js","anon"); sfcd.addPathDefinition("/images","anon"); sfcd.addPathDefinition("/fonts","anon"); sfcd.addPathDefinition("/html","anon"); //不是anon表示需要进行拦截,进行校验和控制的 sfcd.addPathDefinition("/logout","logout"); sfcd.addPathDefinition("/**","user"); return sfcd; } @Bean public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator(); /** * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。 * 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。 * 加入这项配置能解决这个bug */ defaultAdvisorAutoProxyCreator.setUsePrefix(true); return defaultAdvisorAutoProxyCreator; } }
3 Realm域对象的建立,就是上面那个bean里面。存储从数据库读取数据的对象
package org.victor.shirodemo_01.pojo; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import lombok.extern.slf4j.Slf4j; 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.springframework.beans.factory.annotation.Autowired; import org.victor.shirodemo_01.common.Log; import org.victor.shirodemo_01.mapper.PermissionMapper; import org.victor.shirodemo_01.mapper.RoleMapper; import org.victor.shirodemo_01.mapper.UserMapper; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** * @program: shirodemo_01 * @ClassName ShiroRealm * @description: * @author: victorGang * @create: 2022-09-14 15:23 * @Version 1.0 **/ @Slf4j public class ShiroRealm extends AuthorizingRealm { @Autowired private UserMapper userMapper; @Autowired private RoleMapper roleMapper; @Autowired private PermissionMapper permissionMapper; @Override @Log("校验用户角色权限") protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { log.info("从登录的获取,当前用户的信息,用这个用户去数据库查询角色和权限"); User user = (User) principalCollection.getPrimaryPrincipal(); if (user == null) { throw new AccountException("用户登录校验没有通过,不能进行此操作"); } List<Role> roleList = roleMapper.selectByUserId(user.getUserId()); Set<String> strRoles = roleList.stream() .map(r -> r.getRoleName()) .collect(Collectors.toSet()); List<String> permissions = new ArrayList<>(); if (roleList.size() > 0) { //查找该用户所有的权限 permissions = permissionMapper.selectPermInRoleIds(roleList); }else { throw new AccountException("该用户不具该权限"); } SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo(); authzInfo.setStringPermissions(new HashSet<>(permissions)); authzInfo.setRoles(strRoles); return authzInfo; } @Override @Log("校验用户,去数据库查找输入的用户名对应的对象,返回给controller层的调用者") protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String username = (String) authenticationToken.getPrincipal(); QueryWrapper<User> qw = new QueryWrapper<>(); qw.eq("user_name", username); User user = userMapper.selectOne(qw); if (user != null) { return new SimpleAuthenticationInfo(user, user.getUserPassword(), getClass().getName()); } else { throw new AccountException("用户不存在"); } } }
mapper.xml里面的去数据库查询用户,查询角色授权信息的
//查询用户的全部角色信息,返回角色的集合 <select id="selectByUserId" resultType="org.victor.shirodemo_01.pojo.Role"> select sr.role_id, sr.role_name from shiro_user_role sur join shiro_user su on su.user_id = sur.user_id join shiro_role sr on sr.role_id = sur.role_id where sur.user_id = #{userid} </select> //查询角色的全部授权信息 ,返回的字符串集合 <select id="selectPermInRoleIds" resultType="java.lang.String"> SELECT sp.permission_name FROM shiro_permission_role spr join shiro_permission sp on spr.permission_id=sp.permission_id where spr.role_id in <foreach collection="list" item="demo" open="(" close=")"> #{demo.roleId} </foreach> </select>
package org.victor.shirodemo_01.controller; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AccountException; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.annotation.RequiresPermissions; 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; import org.victor.shirodemo_01.common.ResultUtil; /** * @program: shirodemo_01 * @ClassName LoginController * @description: * @author: victorGang * @create: 2022-09-14 15:25 * @Version 1.0 **/ @RestController @Slf4j public class LoginController { @PostMapping("/login") public ResultUtil login(String username, String password) { Subject subject = SecurityUtils.getSubject(); try { subject.login(new UsernamePasswordToken(username, password)); return ResultUtil.success(); } catch (AuthenticationException e) { if (e.getClass() == AccountException.class) { throw e; } else { throw new AccountException("用户名或者密码错误"); } } } @GetMapping("/pay") public String pay() { return "success,已经登录成功,成功进入支付页面"; } @GetMapping("/doHomeWork") @RequiresPermissions("doHomeWork") public ResultUtil doHomeWork() { return ResultUtil.success(200,"用户拥有doHomeWork权限"); } }
package org.victor.shirodemo_01.controller.component; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.ShiroException; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.victor.shirodemo_01.common.MyException; import org.victor.shirodemo_01.common.ResultUtil; import java.io.IOException; @RestControllerAdvice//restful风格,全局异常处理的,返回json @Slf4j public class GlobExceptionHandle { @ExceptionHandler(Exception.class)//全局处理系统异常 public ResultUtil handleException(Exception e) { // e.printStackTrace(); log.error("系统异常:" + e.getMessage(), e); return ResultUtil.error(001, "系统严重错误"); } @ExceptionHandler(value = MethodArgumentNotValidException.class) public ResultUtil handler(MethodArgumentNotValidException e) throws IOException { /** @Author victorGang * @Description //TODO 全局异常处理,参数校验的,判断前端传过来的参数符不符合要求 * @Date 19:47 2022/9/3 * @Param [e] * @return org.victor.test_paper_project.common.ResultUtil **/ BindingResult bindingResult = e.getBindingResult(); ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get(); String messages = objectError.getDefaultMessage(); log.warn("MethodArgumentNotValidException异常:-------------->{}", messages); return ResultUtil.error().setData(messages); } /** * 拦截捕捉自定义异常 ConstraintViolationException.class * @param * @return */ // @ExceptionHandler(value = ConstraintViolationException.class) // public ResultUtil ConstraintViolationExceptionHandler(ConstraintViolationException ex) { // Map map = new HashMap(); // Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations(); // Iterator<ConstraintViolation<?>> iterator = constraintViolations.iterator(); // List<String> msgList = new ArrayList<>(); // while (iterator.hasNext()) { // ConstraintViolation<?> cvl = iterator.next(); // msgList.add(cvl.getMessageTemplate()); // } // // map.put("msg", msgList); // return ResultUtil.error(201,"参数校验异常").setData(map); // } @ExceptionHandler(ShiroException.class)//全局处理 安全框架异常 public ResultUtil handleShiroException(ShiroException e) { log.warn("安全框架shiro异常:" + e.getMessage(), e); return ResultUtil.error(405, e.getMessage()); } @ExceptionHandler(MyException.class)//全局处理 自定义异常 public ResultUtil handleMyException(MyException e) { log.warn("业务异常:" + e.getMsg(), e); return ResultUtil.error(e.getCode(), e.getMsg()); } }
package org.victor.shirodemo_01.common; import java.lang.annotation.*; /** * 自定义日志说明注解,用在Controller类的方法上 * @author liwei * 只要controller层的方法上,标记这个自定义注解,并且把这个注解在注解切面里设置了, * 那么只要执行了这个方法,就会自动把这个方法,入参,执行的返回值,消耗时间,打印出来 * */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Log { /* * 业务名称 */ String value() default ""; }
日志切面
package org.victor.shirodemo_01.common; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; /** * 接口执行日志切面类,前提:(1)需要确保类路径存在依赖Aspect-starer-aop(自己引入)和logback(spring自带了)。 *(2)自行注册到spring容器 @Component * @author liwei,lucas * */ @Aspect @Slf4j @Component public class LogAspect { /** * 通过环绕通知在接口执行前后输出日志信息 把环绕通知,绑定给有自定义注解的方法。以自定义注解的全路径绑定 * @param proceedingJoinPoint * @return * @throws Throwable * */ @Around("@annotation(org.victor.shirodemo_01.common.Log)") //环绕通知里面放切入点表达式,这里是直接放的自定义注解,只要被这个注解标记了的方法,就能执行这个环绕通知 public Object logOutputInfo(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { MethodSignature ms = (MethodSignature)proceedingJoinPoint.getSignature(); String operation = ms.getMethod().getAnnotation(org.victor.shirodemo_01.common.Log.class).value(); StopWatch stopWatch = new StopWatch(); stopWatch.start(); String finalOp = operation.trim().length() == 0 ? "" : "[" + operation + "]"; log.info("执行业务"+ finalOp +",参数:{} ",proceedingJoinPoint.getArgs()); Object result = proceedingJoinPoint.proceed(); stopWatch.stop(); log.info("业务方法"+ finalOp +"执行完毕返回数据: {},耗时{}ms",result,stopWatch.getTotalTimeMillis()); return result; } }