架构
要学习如何使用Shiro必须先从它的架构谈起,作为一款安全框架Shiro的设计相当精妙。Shiro的应用不依赖任何容器,它也可以在JavaSE下使用。但是最常用的环境还是JavaEE。下面以用户登录为例:
1)使用用户的登录信息创建令牌
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token可以理解为用户令牌,登录的过程被抽象为Shiro验证令牌是否具有合法身份以及相关权限。
2)执行登陆动作
SecurityUtils.setSecurityManager(securityManager); // 注入SecurityManager
Subject subject = SecurityUtils.getSubject(); // 获取Subject单例对象
subject.login(token); // 登陆
Shiro的核心部分是SecurityManager,它负责安全认证与授权。Shiro本身已经实现了所有的细节,用户可以完全把它当做一个黑盒来使用。SecurityUtils对象,本质上就是一个工厂类似Spring中的ApplicationContext。Subject是初学者比较难于理解的对象,很多人以为它可以等同于User,其实不然。Subject中文翻译:项目,而正确的理解也恰恰如此。它是你目前所设计的需要通过Shiro保护的项目的一个抽象概念。通过令牌(token)与项目(subject)的登陆(login)关系,Shiro保证了项目整体的安全。
3)判断用户
Shiro本身无法知道所持有令牌的用户是否合法,因为除了项目的设计人员恐怕谁都无法得知。因此Realm是整个框架中为数不多的必须由设计者自行实现的模块,当然Shiro提供了多种实现的途径,本文只介绍最常见也最重要的一种实现方式——数据库查询。
4)认证(AuthenticationInfo)和授权(AuthorizationInfo)
在解释它们前首先必须要描述一下Shiro对于安全用户的界定:和大多数操作系统一样。用户具有角色和权限两种最基本的属性。例如,我的Windows登陆名称是amigo,它的角色是administrator,而administrator具有所有系统权限。这样amigo自然就拥有了所有系统权限。那么其他人需要登录我的电脑怎么办,我可以开放一个guest角色,任何无法提供正确用户名与密码的未知用户都可以通过guest来登录,而系统对于guest角色开放的权限极其有限。
同理,Shiro对用户的约束也采用了这样的方式。AuthenticationInfo代表了用户的角色信息集合,AuthorizationInfo代表了角色的权限信息集合。如此一来,当设计人员对项目中的某一个url路径设置了只允许某个角色或具有某种权限才可以访问的控制约束的时候,Shiro就可以通过以上两个对象来判断。
实现Realm
如何实现Realm是本文的重头戏,也是比较费事的部分。这里大家会接触到几个新鲜的概念:缓存机制、散列算法、加密算法。
1)缓存机制
Ehcache是很多Java项目中使用的缓存框架,Hibernate就是其中之一。它的本质就是将原本只能存储在内存中的数据通过算法保存到硬盘上,再根据需求依次取出。你可以把Ehcache理解为一个Map<String,Object>对象,通过put保存对象,再通过get取回对象。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="shirocache">
<diskStore path="java.io.tmpdir" />
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="1800"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
以上是ehcache.xml文件的基础配置,timeToLiveSeconds为缓存的最大生存时间,timeToIdleSeconds为缓存的最大空闲时间,当eternal为false时ttl和tti才可以生效。
2)散列算法与加密算法
md5是本文会使用的散列算法,加密算法本文不会涉及。散列和加密本质上都是将一个Object变成一串无意义的字符串,不同点是经过散列的对象无法复原,是一个单向的过程。例如,对密码的加密通常就是使用散列算法,因此用户如果忘记密码只能通过修改而无法获取原始密码。但是对于信息的加密则是正规的加密算法,经过加密的信息是可以通过秘钥解密和还原。
3)用户注册
请注意,虽然我们一直在谈论用户登录的安全性问题,但是说到用户登录首先就是用户注册。如何保证用户注册的信息不丢失,不泄密也是项目设计的重点。
public class PasswordHelper {
private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
private String algorithmName = "md5";
private final int hashIterations = 2;
public void encryptPassword(User user) {
// User对象包含最基本的字段Username和Password
user.setSalt(randomNumberGenerator.nextBytes().toHex());
// 将用户的注册密码经过散列算法替换成一个不可逆的新密码保存进数据,散列过程使用了盐
String newPassword = new SimpleHash(algorithmName, user.getPassword(),
ByteSource.Util.bytes(user.getCredentialsSalt()), hashIterations).toHex();
user.setPassword(newPassword);
}
}
如果你不清楚什么叫加盐可以忽略散列的过程,只要明白存储在数据库中的密码是根据户注册时填写的密码所产生的一个新字符串就可以了。经过散列后的密码替换用户注册时的密码,然后将User保存进数据库。剩下的工作就丢给UserService来处理。
那么这样就带来了一个新问题,既然散列算法是无法复原的,当用户登录的时候使用当初注册时的密码,我们又应该如何判断?答案就是需要对用户密码再次以相同的算法散列运算一次,再同数据库中保存的字符串比较。
4)匹配
CredentialsMatcher是一个接口,功能就是用来匹配用户登录使用的令牌和数据库中保存的用户信息是否匹配。
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
// 声明一个缓存接口,这个接口是Shiro缓存管理的一部分,它的具体实现可以通过外部容器注入
private Cache<String, AtomicInteger> passwordRetryCache;
public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal();
AtomicInteger retryCount = passwordRetryCache.get(username);
if (retryCount == null) {
retryCount = new AtomicInteger(0);
passwordRetryCache.put(username, retryCount);
}
// 自定义一个验证过程:当用户连续输入密码错误5次以上禁止用户登录一段时间
if (retryCount.incrementAndGet() > 5) {
throw new ExcessiveAttemptsException();
}
boolean match = super.doCredentialsMatch(token, info);
if (match) {
passwordRetryCache.remove(username);
}
return match;
}
}
可以看到,这个实现里设计人员仅仅是增加了一个不允许连续错误登录的判断。真正匹配的过程还是交给它的直接父类去完成。连续登录错误的判断依靠Ehcache缓存来实现。显然match返回true为匹配成功。
5)获取用户的角色和权限信息
public class UserRealm extends AuthorizingRealm {
// 用户对应的角色信息与权限信息都保存在数据库中,通过UserService获取数据
private UserService userService = new UserServiceImpl();
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 根据用户名查询当前用户拥有的角色
Set<Role> roles = userService.findRoles(username);
Set<String> roleNames = new HashSet<String>();
for (Role role : roles) {
roleNames.add(role.getRole());
}
// 将角色名称提供给info
authorizationInfo.setRoles(roleNames);
// 根据用户名查询当前用户权限
Set<Permission> permissions = userService.findPermissions(username);
Set<String> permissionNames = new HashSet<String>();
for (Permission permission : permissions) {
permissionNames.add(permission.getPermission());
}
// 将权限名称提供给info
authorizationInfo.setStringPermissions(permissionNames);
return authorizationInfo;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
User user = userService.findByUsername(username);
if (user == null) {
// 用户名不存在抛出异常
throw new UnknownAccountException();
}
if (user.getLocked() == 0) {
// 用户被管理员锁定抛出异常
throw new LockedAccountException();
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(),
user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), getName());
return authenticationInfo;
}
}
Shiro需要根据用户名和密码首先判断登录的用户是否合法,然后再对合法用户授权。而这个过程就是Realm的实现过程。
6)会话
用户的一次登录即为一次会话,Shiro也可以代替Tomcat等容器管理会话。目的是当用户停留在某个页面长时间无动作的时候,再次对任何链接的访问都会被重定向到登录页面要求重新输入用户名和密码而不需要程序员在Servlet中不停的判断Session中是否包含User对象。启用Shiro会话管理的另一个用途是可以针对不同的模块采取不同的会话处理。以淘宝为例,用户注册淘宝以后可以选择记住用户名和密码。之后再次访问就无需登陆。但是如果你要访问支付宝或购物车等链接依然需要用户确认身份。当然,Shiro也可以创建使用容器提供的Session做为实现。
认证执行流程
1)通过ini配置文件创建securityManager
2)调用subject.login方法主体提交认证,提交的token
3)securityManager进行认证,securityManager最终由ModularRealmAuthenticator进行认证。
4)ModularRealmAuthenticator调用IniRealm(给realm传入token) 去ini配置文件中查询用户信息
5)IniRealm根据输入的token(UsernamePasswordToken)从 shiro.ini查询用户信息,根据账号查询用户信息(账号和密码)
如果查询到用户信息,就给ModularRealmAuthenticator返回用户信息(账号和密码)
如果查询不到,就给ModularRealmAuthenticator返回null
6)ModularRealmAuthenticator接收IniRealm返回Authentication认证信息
如果返回的认证信息是null,ModularRealmAuthenticator抛出异常(org.apache.shiro.authc.UnknownAccountException)
如果返回的认证信息不是null(说明inirealm找到了用户),对IniRealm返回用户密码 (在ini文件中存在)
和 token中的密码 进行对比,如果不一致抛出异常(org.apache.shiro.authc.IncorrectCredentialsException)
授权执行流程
1)对subject进行授权,调用方法isPermitted("permission串")
2)SecurityManager执行授权,通过ModularRealmAuthorizer执行授权
3)ModularRealmAuthorizer执行realm(自定义的Realm)从数据库查询权限数据
调用realm的授权方法:doGetAuthorizationInfo
4)realm从数据库查询权限数据,返回ModularRealmAuthorizer
5)ModularRealmAuthorizer调用PermissionResolver进行权限串比对
6)如果比对后,isPermitted中"permission串"在realm查询到权限数据中,说明用户访问permission串有权限,否则 没有权限,抛出异常。
SpringMVC集成
1)配置前端过滤器
先说一个题外话,Filter是过滤器,interceptor是拦截器。前者基于回调函数实现,必须依靠容器支持。因为需要容器装配好整条FilterChain并逐个调用。后者基于代理实现,属于AOP的范畴。
web.xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>Shiro_Project</display-name>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<!-- 将Shiro的配置文件交给Spring监听器初始化 -->
<param-value>classpath:spring.xml,classpath:spring-shiro-web.xml</param-value>
</context-param>
<context-param>
<param-name>log4jConfigLoaction</param-name>
<param-value>classpath:log4j.properties</param-value>
</context-param>
<!-- shiro配置 开始 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- shiro配置 结束 -->
</web-app>
由于项目通过Spring管理,因此所有的配置原则上都是交给Spring。DelegatingFilterProxy的功能是通知Spring将所有的Filter交给ShiroFilter管理。
spring-shiro-web.xml 配置
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<!-- 缓存管理器 使用Ehcache实现 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml" />
</bean>
<!-- 凭证匹配器 -->
<bean id="credentialsMatcher" class="utils.RetryLimitHashedCredentialsMatcher">
<constructor-arg ref="cacheManager" />
<property name="hashAlgorithmName" value="md5" />
<property name="hashIterations" value="2" />
<property name="storedCredentialsHexEncoded" value="true" />
</bean>
<!-- Realm实现 -->
<bean id="userRealm" class="utils.UserRealm">
<property name="credentialsMatcher" ref="credentialsMatcher" />
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="userRealm" />
</bean>
<!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/" />
<property name="unauthorizedUrl" value="/" />
<property name="filterChainDefinitions">
<value>
/authc/admin = roles[admin]
/authc/** = authc
/** = anon
</value>
</property>
</bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
</beans>
需要注意filterChainDefinitions过滤器中对于路径的配置是有顺序的,当找到匹配的条目之后容器不会再继续寻找。因此带有通配符的路径要放在后面。三条配置的含义是: /authc/admin需要用户有用admin权限、/authc/**用户必须登录才能访问、/**其他所有路径任何人都可以访问。
系统登录控制层
@Controller
public class LoginController {
@Autowired
private UserService userService;
@RequestMapping("login")
public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
} catch (IncorrectCredentialsException ice) {
// 捕获密码错误异常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "password error!");
return mv;
} catch (UnknownAccountException uae) {
// 捕获未知用户名异常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "username error!");
return mv;
} catch (ExcessiveAttemptsException eae) {
// 捕获错误登录过多的异常
ModelAndView mv = new ModelAndView("error");
mv.addObject("message", "times error");
return mv;
}
User user = userService.findByUsername(username);
subject.getSession().setAttribute("user", user);
return new ModelAndView("success");
}
}
登录完成以后,当前用户信息被保存进Session。这个Session是通过Shiro管理的会话对象,要获取依然必须通过Shiro。Session中不存在User对象。
@Controller
@RequestMapping("authc")
public class AuthcController {
// /authc/** = authc 任何通过表单登录的用户都可以访问
@RequestMapping("anyuser")
public ModelAndView anyuser() {
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getSession().getAttribute("user");
System.out.println(user);
return new ModelAndView("inner");
}
// /authc/admin = user[admin] 只有具备admin角色的用户才可以访问,否则请求将被重定向至登录界面
@RequestMapping("admin")
public ModelAndView admin() {
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getSession().getAttribute("user");
System.out.println(user);
return new ModelAndView("inner");
}
}
SpringBoot集成
pom.xml 配置
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.learnhow.springboot</groupId>
<artifactId>web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>web</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</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-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml 配置
server:
port: 8088
spring:
application:
name: shiro
datasource:
url: jdbc:mysql://localhost:3306/shiro
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
jpa:
database: mysql
showSql: true
hibernate:
ddlAuto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5Dialect
format_sql: true
创建用户、角色和权限类
import java.io.Serializable;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
@Entity
@Table(name = "user_t")
public class User implements Serializable {
private static final long serialVersionUID = -3320971805590503443L;
@Id
@GeneratedValue
private long id;
private String username;
private String password;
private String salt;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_role_t", joinColumns = { @JoinColumn(name = "uid") }, inverseJoinColumns = {
@JoinColumn(name = "rid") })
private List<SysRole> roles;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public List<SysRole> getRoles() {
return roles;
}
public void setRoles(List<SysRole> roles) {
this.roles = roles;
}
public String getCredentialsSalt() {
return username + salt + salt;
}
@Override
public String toString() {
return "User [id=" + id + ", username=" + username + "]";
}
}
import java.io.Serializable;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
@Entity
@Table(name = "role_t")
public class SysRole implements Serializable {
private static final long serialVersionUID = -8687790154329829056L;
@Id
@GeneratedValue
private Integer id;
private String role;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "role_permission_t", joinColumns = { @JoinColumn(name = "rid") }, inverseJoinColumns = {
@JoinColumn(name = "pid") })
private List<SysPermission> permissions;
@ManyToMany
@JoinTable(name = "user_role_t", joinColumns = { @JoinColumn(name = "rid") }, inverseJoinColumns = {
@JoinColumn(name = "uid") })
private List<User> users;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public List<SysPermission> getPermissions() {
return permissions;
}
public void setPermissions(List<SysPermission> permissions) {
this.permissions = permissions;
}
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
this.users = users;
}
}
import java.io.Serializable;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
@Entity
@Table(name = "permission_t")
public class SysPermission implements Serializable {
private static final long serialVersionUID = 353629772108330570L;
@Id
@GeneratedValue
private Integer id;
private String name;
@ManyToMany
@JoinTable(name = "role_permission_t", joinColumns = { @JoinColumn(name = "pid") }, inverseJoinColumns = {
@JoinColumn(name = "rid") })
private List<SysRole> roles;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<SysRole> getRoles() {
return roles;
}
public void setRoles(List<SysRole> roles) {
this.roles = roles;
}
}
添加测试数据
INSERT INTO `permission_t` VALUES (1, 'Retrieve');
INSERT INTO `permission_t` VALUES (2, 'Create');
INSERT INTO `permission_t` VALUES (3, 'Update');
INSERT INTO `permission_t` VALUES (4, 'Delete');
INSERT INTO `role_t` VALUES (1, 'guest');
INSERT INTO `role_t` VALUES (2, 'user');
INSERT INTO `role_t` VALUES (3, 'admin');
INSERT INTO `role_permission_t` VALUES (1, 1);
INSERT INTO `role_permission_t` VALUES (1, 2);
INSERT INTO `role_permission_t` VALUES (2, 2);
INSERT INTO `role_permission_t` VALUES (3, 2);
INSERT INTO `role_permission_t` VALUES (1, 3);
INSERT INTO `role_permission_t` VALUES (2, 3);
INSERT INTO `role_permission_t` VALUES (3, 3);
INSERT INTO `role_permission_t` VALUES (4, 3);
至此,前期的准备工作已经完成。下面为了让Shiro能够在项目中生效我们需要通过代码的方式提供配置信息。Shiro的安全管理提供了两个层面的控制:(1)用户认证:需要用户通过登陆证明你是你自己。(2)权限控制:在证明了你是你自己的基础上系统为当前用户赋予权限。后者我们已经在数据库中完成了大部分配置。
用户认证的常规手段就是登陆认证,在目前的情况下我们认为只有用户自己知道登陆密码。不过Shiro为我们做的更多,它还提供了一套能够很方便我们使用的密码散列算法。因为普通的散列技巧可以很容易的通过暴力手段破解,我们可以在散列的过程中加入一定的算法复杂度(增加散列次数与Salt)从而解决这样的问题。
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
import com.learnhow.springboot.web.entity.User;
public class PasswordHelper {
private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
public static final String ALGORITHM_NAME = "md5"; // 基础散列算法
public static final int HASH_ITERATIONS = 2; // 自定义散列次数
public void encryptPassword(User user) {
// 随机字符串作为salt因子,实际参与运算的salt我们还引入其它干扰因子
user.setSalt(randomNumberGenerator.nextBytes().toHex());
String newPassword = new SimpleHash(ALGORITHM_NAME, user.getPassword(),
ByteSource.Util.bytes(user.getCredentialsSalt()), HASH_ITERATIONS).toHex();
user.setPassword(newPassword);
}
}
这个类帮助我们解决用户注册的密码散列问题,当然我们还需要使用同样的算法来保证在登陆的时候密码能够被散列成相同的字符串。如果两次散列的结果不同系统就无法完成密码比对,因此在计算散列因子的时候我们不能引入变量,例如我们可以将username作为salt因子加入散列算法,但是不能选择password或datetime,具体原因各位请手动测试。
另外为了帮助Shiro能够正确为当前登陆用户做认证和赋权,我们需要实现自定义的Realm。具体来说就是实现doGetAuthenticationInfo和doGetAuthorizationInfo,这两个方法前者负责登陆认证后者负责提供一个权限信息。
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;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import com.learnhow.springboot.web.entity.SysPermission;
import com.learnhow.springboot.web.entity.SysRole;
import com.learnhow.springboot.web.entity.User;
import com.learnhow.springboot.web.service.UserService;
public class EnceladusShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String username = (String) principals.getPrimaryPrincipal();
User user = userService.findUserByName(username);
for (SysRole role : user.getRoles()) {
authorizationInfo.addRole(role.getRole());
for (SysPermission permission : role.getPermissions()) {
authorizationInfo.addStringPermission(permission.getName());
}
}
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
User user = userService.findUserByName(username);
if (user == null) {
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),
ByteSource.Util.bytes(user.getCredentialsSalt()), getName());
return authenticationInfo;
}
}
还记得前面我们说过,认证的时候我们需要提供相同的散列算法吗?可是在上面的代码里,我们并未提供。那么Shiro是怎么做的呢?AuthorizingRealm是一个抽象类,我们会在另外的配置文件里向它提供基础算法与散列次数这两个变量。
import java.util.HashMap;
import java.util.Map;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> filterChainDefinitionMap = new HashMap<String, String>();
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthc");
shiroFilterFactoryBean.setSuccessUrl("/home/index");
filterChainDefinitionMap.put("/*", "anon");
filterChainDefinitionMap.put("/authc/index", "authc");
filterChainDefinitionMap.put("/authc/admin", "roles[admin]");
filterChainDefinitionMap.put("/authc/renewable", "perms[Create,Update]");
filterChainDefinitionMap.put("/authc/removable", "perms[Delete]");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName(PasswordHelper.ALGORITHM_NAME); // 散列算法
hashedCredentialsMatcher.setHashIterations(PasswordHelper.HASH_ITERATIONS); // 散列次数
return hashedCredentialsMatcher;
}
@Bean
public EnceladusShiroRealm shiroRealm() {
EnceladusShiroRealm shiroRealm = new EnceladusShiroRealm();
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); // 原来在这里
return shiroRealm;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
return securityManager;
}
@Bean
public PasswordHelper passwordHelper() {
return new PasswordHelper();
}
}
接下来,我们将目光集中到上文的shirFilter方法中。Shiro通过一系列filter来控制访问权限,并在它的内部为我们预先定义了多个过滤器,我们可以直接通过字符串配置这些过滤器。
常用的过滤器如下:
authc:所有已登陆用户可访问
roles:有指定角色的用户可访问,通过[ ]指定具体角色,这里的角色名称与数据库中配置一致
perms:有指定权限的用户可访问,通过[ ]指定具体权限,这里的权限名称与数据库中配置一致
anon:所有用户可访问,通常作为指定页面的静态资源时使用
为了测试方便我们不引入页面配置直接通过rest方式访问
不受权限控制访问的地址
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.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.learnhow.springboot.web.PasswordHelper;
import com.learnhow.springboot.web.entity.User;
import com.learnhow.springboot.web.service.UserService;
@RestController
@RequestMapping
public class HomeController {
@Autowired
private UserService userService;
@Autowired
private PasswordHelper passwordHelper;
@GetMapping("login")
public Object login() {
return "Here is Login page";
}
@GetMapping("unauthc")
public Object unauthc() {
return "Here is Unauthc page";
}
@GetMapping("doLogin")
public Object doLogin(@RequestParam String username, @RequestParam String password) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
} catch (IncorrectCredentialsException ice) {
return "password error!";
} catch (UnknownAccountException uae) {
return "username error!";
}
User user = userService.findUserByName(username);
subject.getSession().setAttribute("user", user);
return "SUCCESS";
}
@GetMapping("register")
public Object register(@RequestParam String username, @RequestParam String password) {
User user = new User();
user.setUsername(username);
user.setPassword(password);
passwordHelper.encryptPassword(user);
userService.saveUser(user);
return "SUCCESS";
}
}
需要指定权限可以访问的地址
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.learnhow.springboot.web.entity.User;
@RestController
@RequestMapping("authc")
public class AuthcController {
@GetMapping("index")
public Object index() {
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getSession().getAttribute("user");
return user.toString();
}
@GetMapping("admin")
public Object admin() {
return "Welcome Admin";
}
// delete
@GetMapping("removable")
public Object removable() {
return "removable";
}
// insert & update
@GetMapping("renewable")
public Object renewable() {
return "renewable";
}
}