springboot + Shiro + thymeleaf
什么是Shiro?
- Apache Shiro是一个java的安全(权限)框架
- Shiro可以非常容易开发出足够好的应用,其不仅可以用在javaSE环境,也可以用在javaEE环境
- Shiro可以完成认证、授权、会话管理、web集成、缓存等
有哪些功能?
Authentication:用户认证(登录)
Authorization:权限控制
Session Management:会话管理
Cryptography:数据加密
Web Support:支持web的API
Caching:缓存
Concurrency:支持多线程应用程序
Testing:测试的支持
“Run As”:假设一个用户为另一个用户的身份
“Remember Me”:在Session中保存用户身份
可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;
其每个 API 的含义:
Subject :主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互
的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定
到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认
为是一个门面;SecurityManager 才是实际的执行者;
SecurityManager :安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;
且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行
交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;
Realm: :域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager
要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;
也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看
成 DataSource,即安全数据源。
也就是说对于我们而言,最简单的一个 Shiro 应用:
1、 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
2、 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法
的用户及其权限进行判断。
接下来我们来从 Shiro 内部来看下 Shiro 的架构
shiro整合springboot
-
新建项目,确保导入依赖
<?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.3.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.ryan</groupId> <artifactId>springboot-shiro</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-shiro</name> <description>Demo project for Spring Boot</description> <properties> <java.version>11</java.version> </properties> <dependencies> <!--shiro-spring --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.3</version> </dependency> <!--数据库驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <!--shiro整合thymeleaf--> <!--thymeleaf-extras-shiro --> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency> <!--mybatis-springboot--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <!--druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.22</version> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </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>
-
创建数据库
-
配置yaml/properties文件(整合mybatis)
spring: datasource: username: root password: 1227 url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
mybatis.type-aliases-package=com.ryan.pojo mybatis.mapper-locations=classpath:mapper/*.xml
-
编写实体类
package com.ryan.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; private String username; private String password; private String perms; }
-
编写mapper接口和mapper.xml(此文件放在classpath的mapper目录下)
package com.ryan.mapper; import com.ryan.pojo.User; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Repository @Mapper public interface UserMapper { User queryUserByName(String username); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ryan.mapper.UserMapper"> <select id="queryUserByName" resultType="user"> select * from mybatis.user where username=#{username} </select> </mapper>
-
导入静态资源:略
-
编写controller并测试环境
package com.ryan.controller; import org.apache.catalina.security.SecurityUtil; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class UserController { @RequestMapping({"/","/index"}) public String index(Model model){ model.addAttribute("msg","hello,Shiro"); return "index"; } @RequestMapping("/user/add") public String add(){ return "/user/add"; } @RequestMapping("/user/update") public String update(){ return "/user/update"; } @RequestMapping("/toLogin") public String toLogin(){ return "login"; } @RequestMapping("/login") public String login(String username, String password,Model model){ //获取当前用户 Subject currentUser = SecurityUtils.getSubject(); //封装用户的账号密码 UsernamePasswordToken token = new UsernamePasswordToken(username, password); //执行登陆方法,如果用户名或者密码有误则分别抛出异常,如果正确则进入首页 try { currentUser.login(token); model.addAttribute("msg",username); return "index"; }catch (UnknownAccountException e){ model.addAttribute("msg","用户名错误"); return "login"; }catch (IncorrectCredentialsException e){ model.addAttribute("msg","密码错误"); return "login"; } } @RequestMapping("/unauthorized") @ResponseBody public String unauthorizedUrl(){ return "抱歉,未经授权无法访问次网页"; } }
-
编写config
package com.ryan.config; import com.ryan.pojo.User; import com.ryan.service.UserService; import com.ryan.service.UserServiceImpl; import org.apache.shiro.SecurityUtils; 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.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; //自定义一个Realm,只需要继承AuthorizingRealm类并重写方法即可 public class UserRealm extends AuthorizingRealm { @Autowired UserServiceImpl userService; //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了=>doGetAuthorizationInfo方法"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Subject subject = SecurityUtils.getSubject(); User currentUser = (User) subject.getPrincipal();//获取当前用户并强转 String perms = currentUser.getPerms();//需要携带权限 info.addStringPermission(perms);//有权限的才会房型 return info; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了=>doGetAuthenticationInfo方法"); UsernamePasswordToken userToken = (UsernamePasswordToken) token; User user = userService.queryUserByName(userToken.getUsername()); if(user == null){ return null;//抛出异常 } //密码错误,shiro来操作,这里的principal为user,这样在授权也可以获取到此用户 return new SimpleAuthenticationInfo(user,user.getPassword(),""); } }
package com.ryan.config; import at.pollux.thymeleaf.shiro.dialect.ShiroDialect; import org.apache.catalina.Realm; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; import java.util.Map; @Configuration public class ShiroConfig { //第三步:ShiroFilterFactoryBean @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultSecurityManager defaultSecurityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); //设置安全管理器 bean.setSecurityManager(defaultSecurityManager); //添加shiro的内置过滤器 /* anon:无需认证就可以访问 authc:必须认证了才能访问 user:必须拥有 记住我 功能才能使用 perms:拥有对某个资源的权限才能访问 role:拥有某个角色权限才能访问 */ Map<String, String> filterMap = new LinkedHashMap<>(); //设置授权,注意,授权要设置在认证之前,经测试如果放在认证后面会失效 //添加了授权设置之后,应该在数据库的用户表中添加授权数据,并在UserRealm接收和判断 filterMap.put("/user/add","perms[add]"); filterMap.put("/user/update","perms[update]"); //filterMap.put("/user/add","authc"); //filterMap.put("/user/update","authc"); //获取使用通配符 filterMap.put("/user/*","authc"); bean.setFilterChainDefinitionMap(filterMap); //设置登陆页面 bean.setLoginUrl("/toLogin"); //设置未授权页面 bean.setUnauthorizedUrl("/unauthorized"); return bean; } //第二步:SecurityManager //需要传参并绑定userRealm的bean,没有其别名的话默认就是其方法名 @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //关联UserRealm securityManager.setRealm(userRealm); return securityManager; } //第一步:Realm @Bean public UserRealm userRealm(){ //需要另外自定义一个Realm类 return new UserRealm(); } //shiro整合thymeleaf需要以下设置 //添加这段代码的目的就是为了在thymeleaf中使用shiro的自定义tag。 @Bean public ShiroDialect shiroDialect(){ return new ShiroDialect(); } }
-
整合shiro-thymeleaf
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h1>首页</h1> <hr> <p th:text="${msg}"></p> <hr> <!--未登陆--> <!--验证当前用户是否为“访客”,即未认证(包含未记住)的用户--> <div shiro:guest=""> <a th:text="登陆" th:href="@{/toLogin}"></a> </div> <!--已登录--> <!-- 认证通过或已记住的用户--> <div shiro:user=""> <a th:text="注销" th:href="@{/}"></a> </div> <!--是否有add权限--> <div shiro:hasPermission="add"> <a th:href="@{/user/add}">add</a> </div> <!--是否有update权限--> <div shiro:hasPermission="update"> <a th:href="@{/user/update}">update</a> </div> </body> </html>
-
测试:略
学习来源:B站up主狂神说,感觉不错,感兴趣的可以去看下