Shiro简介
什么是Shiro
Apache Shiro是Java的一个安全框架,提供了认证、授权、加密和会话管理等功能。
- Apache Shiro是一个Java的安全(权限)框架。
- Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。
- Shiro可以完成,认证,授权,加密,会话管理,Web集成,缓存等。
- 下载地址: http://shiro.apache.orgl
Shiro的功能
下面介绍一下各功能点的意思:
- Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;就是说判断这个用户是否能做事情,例如:细粒度的验证某个用户对某个资源是否具有某个权限。
- Session Management:回话管理,即用户登录后就是一次会话,在没有退出之前,他的所有信息都在会话中;会话可以是javase环境,也可以是web环境。
- Cryptography:加密,保护数据的安全性;例如:密码加密存储到数据库,而不是明文存储。
- Web Support:web支持,可以非常容易的集成到web环境。
- Caching:缓存,比如用户登录后,用户信息、拥有的角色\权限不必每次都去查,都可以在缓存取,提供效率。
- Concurrency:shiro支持多线程的并发验证,即,如在一个线程中开启另一个线程,可以把权限自动传播过去。
- Testing:提供测试支持;
- Run As:允许一个用户假装为另一个用户(如果他们允许)进行访问。
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
最后要说的是:shiro不会去维护用户、维护权限;这些需要开发者去设计和创建,然后通过响应的接口注入给shiro。
Shiro的架构(外部)
图中有三个十分重要的api,对于所有的Shiro所有的操作基本上都是在这个三个api中完成
- Subject:应用代码直接交互的对象是Subject,也就是说Shiro的对外Api的核心就是Subject,Subject代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等。与Subject的所有交互对象都会委托给SecurityManager;Subject其实是一个门面,SecurityManager才是实际的执行者
- SecurityManager:安全管理器,即所有关于安全的操作都会与SecurityManager交互,并且它管理着所有的Subject,可以看出它是shiro的核心,她负责与后面的其他组件进行交互。可以把他看做SpringMvc的DispatherServlet前端控制器
- Realm:shiro从realm中获取安全数据(如用户,权限,角色),就是说SecurityManager要验证用户身份,那么他要从realm中获取相应的用户进行比较以确认用户身份是否合法;也需要从realm得到用户相应的角色/权限进行验证用户是否能进行操作。可以把Realm看成DataSource,即安全数据源。
简单来说就是以下两点:
- 应用通过Subject来进行认证和授权,而Subject又委托给SecurityManager
- 我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。
Shiro架构(内部)
- Subject:任何可以与应用交互的’用户’
- Security Manager:相当于SpringMVC中的DispatcherServlet;是Shiro的心脏,所有具体的交互都通过SecurityManager进行控制,它管理所有的Subject,且负责进行认证,授权,会话,及缓存的管理。
- Authenticator:负责subject认证,是一个扩展点,可以自定义实现;可以使用认证策略(AuthenticationStrategy),即什么情况下算用户认证通过了
- Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能
- Realm:可以有一个或者多个的realm,可以认为是安全实体数据源,即用于获取安全实体的,可以用JDBC实现,也可以是内存实现等等,由用户提供;所以一般在应用中都需要实现自己的realm
- SessionManager:管理Session生命周期的组件,而shiro并不仅仅可以用在web环境,也可以用在普通的javaSE环境中
- CacheManager:缓存控制器,来管理如用户,角色,权限等缓存的;因为这些数据基本上很少改变,放到缓存中可以提高访问的性能
- Cryptography:密码模块,Shiro提高了一些常见的加密组件用于密码加密,解密等
简单的配置Shiro(整合mybatis)
1.导入依赖(含有mybatis,jdbc,热部署等)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| <?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.3.RELEASE</version> <relativePath/> </parent> <groupId>com.bestrookie</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties>
<dependencies> <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-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency>
<dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring5</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-java8time</artifactId> </dependency>
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
|
2.配置yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource 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,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 mybatis: type-aliases-package: com.bestrookie.pojo mapper-locations: classpath:mapper/*.xml
|
3.写几个简单的html页面
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>首页</h1> <p th:text="${msg}"></p> <a th:href="@{/user/add}">add</a> <a th:href="@{/user/update}">update</a> </body> </html>
|
login.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <!DOCTYPE html> <html lang="en"xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>登录</h1> <hr> <p th:text="${msg}" style="color: red"></p> <form th:action="@{/login}"> <p>用户名<input type="text" name="username"></p> <p>密码<input type="password"name="password"></p> <p><input type="submit"></p> </form> </body> </html>
|
add.html
1 2 3 4 5 6 7 8 9 10
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>add</h1> </body> </html>
|
update.html
1 2 3 4 5 6 7 8 9 10
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>update</h1> </body> </html>
|
4.各个页面对应的controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package com.bestrookie.controller; 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 MyController {
@RequestMapping({"/","/index"}) public String toIndex(Model model){ model.addAttribute("msg","helloword"); 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 subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username,password); try { subject.login(token); return "index"; } catch (UnknownAccountException e) { model.addAttribute("msg","用户名不存在"); return "login"; }catch (IncorrectCredentialsException e){ model.addAttribute("msg","密码错误"); return "login"; } } @RequestMapping("/noauth") @ResponseBody public String unauthorized(){ return "未经授权无法访问此页面"; } }
|
5.对应的mapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package com.bestrookie.mapper;
import com.bestrookie.pojo.User; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository;
@Repository @Mapper public interface UserMapper { public User queryUserByName(String name); }
|
6.既然有mapper当然有对应的xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?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.bestrookie.mapper.UserMapper"> <select id="queryUserByName" parameterType="String" resultMap="userInfo" > select * from mybatis.user where name = #{name} </select> <resultMap id="userInfo" type="User"> <id column="id" property="userId"/> <result column="name" property="userName"/> <result column="password" property="userPassword"/> <result column="perms" property="userPerms"/> </resultMap> </mapper>
|
7.为了符合三层架构得写上我们的service层在这里就不写了就是简单的调用
8.忘了最重要的pojo了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package com.bestrookie.pojo; import lombok.*;
@Data @AllArgsConstructor @NoArgsConstructor public class User { private int userId; private String userName; private String userPassword; private String userPerms; }
|
9.好了重头戏来了配置一个realm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| package com.bestrookie.config; import com.bestrookie.pojo.User; import com.bestrookie.service.UserService; 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;
public class UserRealm extends AuthorizingRealm { @Autowired UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Subject subject = SecurityUtils.getSubject(); User currentUser = (User) subject.getPrincipal(); info.addStringPermission(currentUser.getUserPerms()); return info; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken userToken = (UsernamePasswordToken) token; User user = userService.queryUserByName(userToken.getUsername()); if (user ==null){ return null; }
return new SimpleAuthenticationInfo(user,user.getUserPassword(),""); } }
|
10.最后配置Config
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| package com.bestrookie.config; 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 { @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(defaultWebSecurityManager);
Map<String ,String> filterMap = new LinkedHashMap<>(); filterMap.put("/user/add","perms[user:add]"); filterMap.put("/user/update","perms[user:update]"); filterMap.put("/user/*","authc"); bean.setFilterChainDefinitionMap(filterMap); bean.setLoginUrl("/toLogin"); bean.setUnauthorizedUrl("/noauth"); return bean; } @Bean(name = "getDefaultWebSecurityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm); return securityManager; } @Bean(name = "userRealm") public UserRealm userRealm(){ return new UserRealm(); }
}
|
这个小例子的目录结构+简单数据库设计