7.1 springboot使用Shiro
Shiro是由Apache开源的一款强大的安全框架,本节从了解Shiro框架开始,带领大家学习Spring Boot如何使用Shiro进行身份认证和权限认证。
7.1.1 什么是Shiro
Apache Shiro(官网地址:http://shiro.apache.org/)是一个功能强大且易于使用的Java安全框架,可以利用它进行身份验证、授权、加密和会话管理。通过使用Shiro易于理解的API文档,可以轻松地构建任何应用程序。
如Apache Shiro官网所说,Apache Shiro的首要目标是易于使用和理解。安全有时可能非常复杂,甚至是痛苦的,但并非必须如此。框架应尽可能掩盖复杂性,并提供简洁直观的API,以简化开发人员的工作,并确保其应用程序安全地工作。
以下是Apache Shiro可以做的一些事情:
· 验证用户身份。
· 为用户执行访问控制,例如确定是否为用户分配了某个安全角色或确定是否允许用户执行某些操作。
· 在任何环境中使用Session API,即使没有Web容器或EJB容器也是如此。
· 在身份验证、访问控制或会话生命周期内对事件做出反应。
· 聚合用户安全数据的一个或多个数据源,并将其全部显示为单个复合用户“视图”。
· 启用单点登录(SSO)功能。
· 无须登录即可为用户关联启用“记住我”服务。
Apache Shiro是一个具有许多功能的综合应用程序安全框架,如图7-1所示。
Shiro提供了Shiro开发团队所称的“应用程序安全的4大基石”——身份验证、授权、会话管理和加密。
· 身份认证:其实身份认证可以理解为“登录”。
· 授权:授权是指一些权限的认证,比如管理员可以访问所有页面,但是普通用户只能访问部分页面。
· 会话管理:可以理解为Shiro为我们管理用户的会话(如Session)。
· 加密:使用加密算法来保证数据的安全。
以上是4个主要的功能,如图7-1所示,还提供了其他功能,分别说明如下。
· Web支持:Shiro的Web支持API可帮助用户轻松保护Web应用程序。
· 缓存:Shiro提供了缓存,可以确保安全操作保持快速高效。
· 并发:Apache Shiro支持具有并发功能的多线程应用程序。
· 测试:存在测试支持以帮助用户编写单元和集成测试,并确保代码按预期受到保护。
· 运行方式:允许用户假定其他用户的身份(如果允许)的功能,有时在管理方案中很有用。
· 记住我:记住用户在会话中的身份,这样他们只需要在强制要求时登录。
7.1.2 使用Shiro做权限控制
刚刚介绍了Apache Shiro的基本功能,接下来带领大家学习Spring Boot如何使用Shiro框架进行身份认证和权限管理。
1. 场景及数据库介绍
在创建项目之前,先介绍一下需要实现的场景,数据库表设计如图7-2所示。
其中分为两种角色:admin和user,如果用户角色为admin,则可以进行4个菜单的请求(add、delete、update和select,这里只有select和delete),如果用户角色为user,则只可以进行select请求。如果没有权限,就会跳转到401页面,index页面可以不登录访问。为了方便,默认插入了两个用户:dalaoyang有admin权限;xiaoli有user权限。插入数据脚本如代码清单7-1所示。
2. 依赖配置
接下来我们新建一个项目,由于这里需要使用数据库,因此加入了MySQL和JPA的依赖,模板框架使用的是Thymeleaf,同时加入Shiro依赖,如代码清单7-2所示。
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dalaoyang</groupId>
<artifactId>springboot_shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot_shiro</name>
<description>springboot_shiro</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<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>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.15</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件这里不再赘述,都是关于数据库和JPA的配置,如需查阅,可以在本书源码中查看。
- 实体类及数据操作层
结合上述场景,可以看出user表和role表的关系是多对多,role表和menu表的关系也是多对多,理解了关系,创建实体类就比较容易了。首先创建一个User实体,使用@ManyToMany表明是多对多的关系,在@JoinTable注解中注明中间表的表名以及关联两个表的字段,如代码清单7-3所示。
package com.pbm.entity;
import org.hibernate.validator.constraints.NotEmpty;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
/**
* 用户实体类
* @author Administrator
*
*/
@Entity
public class SysUser implements Serializable {
@Id
@GeneratedValue
private Integer userId;
@NotEmpty
private String userName;
@NotEmpty
private String passWord;
//多对多关系
@ManyToMany(fetch= FetchType.EAGER)
//急加载,加载一个实体时,定义急加载的属性会立即从数据库中加载
//FetchType.LAZY:懒加载,加载一个实体时,定义懒加载的属性不会马上从数据库中加载
@JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "userId") },
inverseJoinColumns ={@JoinColumn(name = "roleId") })
private List<SysRole> roleList;// 一个用户具有多个角色
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
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 List<SysRole> getRoleList() {
return roleList;
}
public void setRoleList(List<SysRole> roleList) {
this.roleList = roleList;
}
}
接下来创建Role实体。和User实体类似,分别注明与User和Menu的多对多关系,如代码清单7-4所示。
package com.pbm.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
/**
* 角色实体类
* @author Administrator
*
*/
@Entity
public class SysRole implements Serializable {
@Id
@GeneratedValue
private Integer roleId;
private String roleName;
//多对多关系
@ManyToMany(fetch= FetchType.EAGER)
@JoinTable(name="SysRoleMenu",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="menuId")})
private List<SysMenu> menuList;
//多对多关系
@ManyToMany
@JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="userId")})
private List<SysUser> userList;// 一个角色对应多个用户
public Integer getRoleId() {
return roleId;
}
public void setRoleId(Integer roleId) {
this.roleId = roleId;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public List<SysMenu> getMenuList() {
return menuList;
}
public void setMenuList(List<SysMenu> menuList) {
this.menuList = menuList;
}
public List<SysUser> getUserList() {
return userList;
}
public void setUserList(List<SysUser> userList) {
this.userList = userList;
}
}
最后是Menu实体,这里表明与Role的多对多关系,如代码清单7-5所示。
package com.pbm.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
/**
* 菜单
* @author Administrator
*
*/
@Entity
public class SysMenu implements Serializable {
@Id
@GeneratedValue
private Integer menuId;
private String menuName;
@ManyToMany
@JoinTable(name="SysRoleMenu",joinColumns={@JoinColumn(name="menuId")},inverseJoinColumns={@JoinColumn(name="roleId")})
private List<SysRole> roleList;
public Integer getMenuId() {
return menuId;
}
public void setMenuId(Integer menuId) {
this.menuId = menuId;
}
public String getMenuName() {
return menuName;
}
public void setMenuName(String menuName) {
this.menuName = menuName;
}
public List<SysRole> getRoleList() {
return roleList;
}
public void setRoleList(List<SysRole> roleList) {
this.roleList = roleList;
}
}
创建一个JPA数据操作层,里面加入一个根据用户名查询用户的方法,如代码清单7-6所示。
package com.pbm.repository;
import org.springframework.data.repository.CrudRepository;
import com.pbm.entity.SysUser;
/**
*
* @author Administrator
*
*/
public interface UserRepository extends CrudRepository<SysUser,Long> {
SysUser findByUserName(String username);
}
4. Shiro配置
创建一个ShiroConfig,然后创建一个shiroFilter方法。在Shiro使用认证和授权时,其实都是通过ShiroFilterFactoryBean设置一些Shiro的拦截器进行的,拦截器会以LinkedHashMap的形式存储需要拦截的资源及链接,并且会按照顺序执行,其中键为拦截的资源或链接,值为拦截的形式(比如authc:所有URL都必须认证通过才可以访问,anon:所有URL都可以匿名访问),在拦截的过程中可以使用通配符,比如/**为拦截所有,所以一般/**放在最下面。同时,可以通过ShiroFilterFactoryBean设置登录链接、未授权链接、登录成功跳转页等,这里设置的shiroFilter方法内容如代码清单7-7所示。
package com.pbm.config;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
/**
*
* @author Administrator
*
*/
@Configuration
public class ShiroConfig {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
logger.info("启动shiroFilter--时间是:" + new Date());
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//shiro拦截器
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
//<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
//<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->
// 配置不被拦截的资源及链接
filterChainDefinitionMap.put("/static/**", "anon");
// 退出过滤器
filterChainDefinitionMap.put("/logout", "logout");
//配置需要认证权限的
filterChainDefinitionMap.put("/**", "authc");
// 如果不设置默认会自动寻找Web工程根目录下的"/login"页面,即本文使用的login.html
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授权界面
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
}
同时,需要在ShiroConfig类中开启shiro aop注解支持,如果没有开启,权限验证就会失效,如代码清单7-8所示。
//开启shiro aop注解支持,不开启的话权限验证就会失效
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
接下来创建一个方法处理一些异常信息,如代码清单7-9所示。
//配置异常处理,不配置的话没有权限后台报错,前台不会跳转到403页面
@Bean(name="simpleMappingExceptionResolver")
public SimpleMappingExceptionResolver
createSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理
mappings.setProperty("UnauthorizedException","403");
simpleMappingExceptionResolver.setExceptionMappings(mappings); // None by default
simpleMappingExceptionResolver.setDefaultErrorView("error"); // No default
simpleMappingExceptionResolver.setExceptionAttribute("ex"); // Default is "exception"
return simpleMappingExceptionResolver;
}
最后,我们需要在ShiroConfig内设置自定义身份认证的Realm,完整ShiroConfig类代码可在本书源代码中查看。MyShiroRealm类代码如代码清单7-10所示。
package com.pbm.config;
import com.pbm.entity.SysMenu;
import com.pbm.entity.SysRole;
import com.pbm.entity.SysUser;
import com.pbm.repository.UserRepository;
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 javax.annotation.Resource;
/**
*
* @author Administrator
*
*/
public class MyShiroRealm extends AuthorizingRealm {
@Resource
private UserRepository userRepository;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUser userInfo = (SysUser)principals.getPrimaryPrincipal();
for(SysRole role:userInfo.getRoleList()){
authorizationInfo.addRole(role.getRoleName());
for(SysMenu menu:role.getMenuList()){
authorizationInfo.addStringPermission(menu.getMenuName());
}
}
return authorizationInfo;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
//获得当前用户的用户名
String username = (String)token.getPrincipal();
System.out.println(token.getCredentials());
//根据用户名找到对象
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
SysUser userInfo = userRepository.findByUserName(username);
if(userInfo == null){
return null;
}
//这里会去校验密码是否正确
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, //用户名
userInfo.getPassWord(),//密码
getName()
);
return authenticationInfo;
}
}
其中,doGetAuthorizationInfo方法用于授权,doGetAuthenticationInfo方法用于验证用户信息,也就是我们常说的登录。
5. 前端页面
本案例中场景设计为5个页面,分别是401页面、delete页面、index页面、login页面及Select页面403页面的代码。如代码清单7-11所示。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
403
</body>
</html>
delete页面的代码如代码清单7-12所示。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
delete
</body>
</html>
index页面中设置了一个注销按钮,如代码清单7-13所示。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
index
<br/>
<form th:action="@{/logout}" method="post">
<p><input type="submit" value="注销"/></p>
</form>
</body>
</html>
login页面通过表单提交数据,如代码清单7-14所示。
账号:
密码:
最后是select页面,代码如代码清单7-15所示。<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
select
</body>
</html>
6. Controller
最后需要创建Controller进行页面跳转,@RequiresPermissions注解设置select方法应该有select权限,@RequiresRoles注解设置delete方法需要有admin的角色。其实Shiro提供了如下几个注解供使用。
· @RequiresAuthentication:表示当前已经通过了身份认证,即Subject. isAuthenticated()返回true。
· @RequiresUser:表示当前用户已经通过身份验证或者通过“记住我”登录的。
· @RequiresRoles:可以通过属性值value设置角色,角色可以设置一个或者多个,并且使用logical属性指定角色需要同时包含多个权限还是只包含一个权限。比如@RequiresRoles (value={“admin”,“user”}, logical= Logical.AND)为当前需要用户同时包含admin和user权限。
· @RequiresPermissions:与上面的注解类似,判断用户是否含有菜单权限,属性值与@RequiresRoles一致。
· @RequiresGuest:表明当前用户没有通过身份验证或通过“记住我”登录过,也就是游客身份。
接下来,我们看一下TestController的代码,需要注意login方法中根据HttpServletRequest获取Shiro处理的异常信息来给出一些提示,比如用户名不存在或者密码错误。完整代码如代码清单7-16所示。
package com.pbm.controller;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
*
- @author Administrator
*/
@Controller
public class TestController {
@GetMapping({"/","/index"})
public String index(){
return"index";
}
@GetMapping("/403")
public String unauthorizedRole(){
return "403";
}
@GetMapping("/delete")
//@RequiresPermissions("delete")
@RequiresRoles("admin")
public String delete(){
return "delete";
}
@GetMapping("/select")
@RequiresPermissions("select")
public String select(){
return "select";
}
@RequestMapping("/login")
public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{
System.out.println("HomeController.login()");
// 登录失败从request中获取shiro处理的异常信息。
// shiroLoginFailure:就是shiro异常类的全类名.
String exception = (String) request.getAttribute("shiroLoginFailure");
String msg = "";
//根据异常判断错误类型
if (exception != null) {
if (UnknownAccountException.class.getName().equals(exception)) {
msg = "账号不存在";
} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
msg = "密码不正确";
} else {
msg = "else >> "+exception;
}
}
map.put("msg", msg);
// 此方法不处理登录成功,由shiro进行处理
return "/login";
}
@GetMapping("/logout")
public String logout(){
return "/login";
}
}
7. 测试
到这里,项目就已经配置完成了。启动项目,这里简单介绍一下笔者用来测试的方法。
(1)在不登录的情况下可以访问index和login页面,访问select页面和delete页面会跳转到login页面。
(2)使用pbm用户登录的话,可以在登录后访问任意页面。
(3)使用xiaoli用户登录的话,除了访问delete页面会跳转到401页面以外,访问其他页面都会正常跳转。
通过以上测试,完全可以测试出Shiro框架做到了认证及授权。读者也可以使用其他方式进行测试,如果读者对Shiro感兴趣,可以在此基础上进行扩展,使用更多的功能。
springboot整合shiro
链接:https://pan.baidu.com/s/1LxTomDJTiNVIoZaFNN3dPw
提取码:fzqs
7.2 使用Spring Security
Spring Security(官网地址:https://spring.io/projects/spring-security)是Spring家族的安全框架,本节将介绍使用Spring Boot结合Spring Security进行身份认证和权限认证。
7.2.1 Spring Security简介
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(Inversion of Control,控制反转)、DI(Dependency Injection,依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Spring Security提供了非常多强大且常用的功能。
· 身份认证:Spring Security提供了多粒度的身份认证,最熟悉的就是我们常用的登录功能。
· 授权:通俗地说,授权是指权限认证。当你向服务器发起一个请求时,服务器会对你的权限进行验证,如果权限不足,就可能重定向到特定的界面或返回HTTP响应码。
· 加密:Spring Security提供了多种加密方式供我们使用(如SHA、MD5或Bcrypt),可以根据需求选择。
· 会话管理:Spring Security拥有特殊的会话管理机制,会对会话进行保护、定期检测会话是否超时等。
· Session管理:如果有需要的话,那么可以配置Spring Security检测无效的Session ID提交并将用户重定向到一个指定的URL。
· 支持HTTP/HTTPS:支持服务器同时使用HTTP和HTTPS,如果需要特殊URL,那么只能使用HTTPS,可以在配置中直接配置进行使用。
· 支持Basic和Digest认证:Basic(基本)验证和Digest(摘要)验证是在Web应用中流行的替代身份验证机制。基本验证是指在客户端请求时,提供用户名和密码验证的一种方式,常见的如EUREKA。摘要认证属于基本认证的升级版,将传输的密码加密,解决了HTTP基本认证不安全的问题。
· Remember-Me:Remember-Me身份验证是指网站能够记住一个主体的身份之间的会话。当第一次登录时,服务器发送cookie给浏览器,浏览器将cookie保存一段时间,当再次访问时,如果在会话中发现cookie,则进行自动登录。
· 提供CSRF解决方案:CSRF是Cross Site Request Forgery的缩写,CSRF是指跨站请求伪造。是一种通过伪装成受信任用户的请求来利用受信任的网站。Spring Security提供了解决这个问题的方案。
· CORS:Spring Security提供了对CORS(跨域资源共享)的支持。
· 安全HTTP响应头:Spring Security支持将各种安全头添加到响应中。
· 匿名身份验证:允许未经过身份验证的用户访问。
除以上介绍的功能之外,还提供了很多功能,有具体需求的读者可以查看官方文档“对症下药”。笔者就不在这里做过多介绍了,毕竟Spring Security不是一节内容能介绍完的。
7.2.2 使用Spring Security做权限控制
接下来还是使用7.1节Spring Boot结合Shiro的场景,使用Spring Boot结合Spring Security实现同样的功能。
1. 场景及初始化数据
场景在这里不做过多介绍,可以查看7.1节的场景。数据库表也没有做大量修改,稍微修改了一下Role表的数据。需要注意,这里由原来的user修改成了ROLE_USER,admin修改成了ROLE_ADMIN(稍后会进行解释)。初始化数据SQL如代码清单7-17所示。
2. 依赖文件及配置文件
新建项目,依赖内容与7-1小节类似,只需要将Shiro依赖替换为Spring Security依赖,完整内容如代码清单7-18所示。
配置文件这里不再赘述,都是关于数据库和JPA的配置,如需查阅,可以在本书源代码处查看。
3. 实体类
实体类也可以采用7.1节的实体类,由于篇幅原因,代码就不再展示了。UserRepository类同样提供了一个findByUserName方法。另外,由于场景需求,需要创建一个RoleRepository,如代码清单7-19所示。
代码清单7-19 Spring Security项目RoleRepository类代码
public interface RoleRepository extends JpaRepository<Role,Integer> {
}
4. SecurityConfig
使用Spring Security进行安全管理需要使SecurityConfig继承WebSecurityConfigurerAdapter类,并且使用HttpSecurity的一切安全策略进行配置。本文使用的配置如下。
· authorizeRequests:配置一些资源或链接的权限认证。
· antMatchers:配置哪些资源或链接需要被认证。
· permitAll:设置完全允许访问的资源和链接。
· hasRole:配置需要认证的资源或链接的角色。需要注意,若这里需要配置权限为USER,则用户需要拥有权限ROLE_USER,这就是初始化脚本中权限内容修改的原因。
· formLogin:设置form表单提交配置。
· loginPage:设置一个自定义的登录页面URL。
· failureUrl:设置一个自定义的登录失败的URL。
· successForwardUrl:设置一个登录成功后自动跳转的URL。
· accessDeniedPage:设置拒绝访问的URL。
· logoutSuccessUrl:设置退出登录的URL。
正如初始化脚本中可以看到的,在数据库中设置了两个用户,笔者在内存中使用configureGlobal设置了两个用户test和admin,其中test用户的初始密码是123,权限为USER,admin用户的初始密码123,权限是ADMIN和USER。由于本文没有给密码设置加密,因此需要定义一个NoOpPasswordEncoder的Bean来设置密码不加密。完整SecurityConfig内容如代码清单7-20所示。
5. MyUserDetailsService
使用数据库认证用户需要自定义一个类来实现UserDetailsService重写loadUserByUsername方法进行认证授权,如代码清单7-21所示。
6. 页面及Controller
页面没有什么改变,基本上和7.1节一致,读者可以查看本书的源代码。接下来创建一个TestController进行页面跳转,如代码清单7-22所示。
7. 测试
推荐的测试方式与7.1节大致一致,这里不再赘述。
7.3 小结
本章学习了Spring Boot和Apache Shiro及Spring Security的整合,虽然整合得深度不高,但是已经将二者结合起来,可以在接下来的工作中有一定的基础。同时,如果要对安全框架进行扩展使用,也可以在本书的基础上使用。
7.2 使用Spring Security
Spring Security(官网地址:https://spring.io/projects/spring-security)是Spring家族的安全框架,本节将介绍使用Spring Boot结合Spring Security进行身份认证和权限认证。
7.2.1 Spring Security简介
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(Inversion of Control,控制反转)、DI(Dependency Injection,依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Spring Security提供了非常多强大且常用的功能。
· 身份认证:Spring Security提供了多粒度的身份认证,最熟悉的就是我们常用的登录功能。
· 授权:通俗地说,授权是指权限认证。当你向服务器发起一个请求时,服务器会对你的权限进行验证,如果权限不足,就可能重定向到特定的界面或返回HTTP响应码。
· 加密:Spring Security提供了多种加密方式供我们使用(如SHA、MD5或Bcrypt),可以根据需求选择。
· 会话管理:Spring Security拥有特殊的会话管理机制,会对会话进行保护、定期检测会话是否超时等。
· Session管理:如果有需要的话,那么可以配置Spring Security检测无效的Session ID提交并将用户重定向到一个指定的URL。
· 支持HTTP/HTTPS:支持服务器同时使用HTTP和HTTPS,如果需要特殊URL,那么只能使用HTTPS,可以在配置中直接配置进行使用。
· 支持Basic和Digest认证:Basic(基本)验证和Digest(摘要)验证是在Web应用中流行的替代身份验证机制。基本验证是指在客户端请求时,提供用户名和密码验证的一种方式,常见的如EUREKA。摘要认证属于基本认证的升级版,将传输的密码加密,解决了HTTP基本认证不安全的问题。
· Remember-Me:Remember-Me身份验证是指网站能够记住一个主体的身份之间的会话。当第一次登录时,服务器发送cookie给浏览器,浏览器将cookie保存一段时间,当再次访问时,如果在会话中发现cookie,则进行自动登录。
· 提供CSRF解决方案:CSRF是Cross Site Request Forgery的缩写,CSRF是指跨站请求伪造。是一种通过伪装成受信任用户的请求来利用受信任的网站。Spring Security提供了解决这个问题的方案。
· CORS:Spring Security提供了对CORS(跨域资源共享)的支持。
· 安全HTTP响应头:Spring Security支持将各种安全头添加到响应中。
· 匿名身份验证:允许未经过身份验证的用户访问。
除以上介绍的功能之外,还提供了很多功能,有具体需求的读者可以查看官方文档“对症下药”。笔者就不在这里做过多介绍了,毕竟Spring Security不是一节内容能介绍完的。
7.2.2 使用Spring Security做权限控制
新建项目,前端页面使用thymeleaf,加入security依赖,pom文件如下:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dalaoyang</groupId>
<artifactId>springboot_security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot_security</name>
<description>springboot整合security</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<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>
</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-security</artifactId>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.15</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件application.properties
##端口号
server.port=8080
##去除thymeleaf的html严格校验
spring.thymeleaf.mode=LEGACYHTML5
#设定thymeleaf文件路径 默认为src/main/resources/templates
spring.freemarker.template-loader-path=classpath:/templates
#设定静态文件路径,js,css等
spring.mvc.static-path-pattern=/static/**
# 是否开启模板缓存,默认true
# 建议在开发时关闭缓存,不然没法看到实时页面
spring.thymeleaf.cache=false
# 模板编码
spring.freemarker.charset=UTF-8
接下来是这篇文章重要的地方,新建一个SecurityConfig类,继承WebSecurityConfigurerAdapter类,重写configure(HttpSecurity httpSecurity)方法,其中/css/**和/index的资源不需要验证,直接可以请求,/user/**的资源需要验证,权限是USER,/admin/**的资源需要验证,权限是ADMIN,登录地址是/login,登录失败地址是/login_error,异常重定向到 /401,注销跳转到/logout。
注入AuthenticationManagerBuilder,在内存中创建一个用户pbm,密码123的用户,权限是USER,代码如下:
package com.pbm.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
*
* @author Administrator
*
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// /css/**和/index的资源不需要验证,直接可以请求
// /user/**的资源需要验证,权限是USER /admin/**的资源需要验证,权限是ADMIN
// 登录地址是/login 登录失败地址是 /login_error
// 异常重定向到 /401
// 注销跳转到 /logout
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
httpSecurity
.authorizeRequests()
.antMatchers("/css/**","/index").permitAll()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin().loginPage("/login").failureUrl("/login_error")
.and()
.exceptionHandling().accessDeniedPage("/401");
httpSecurity.logout().logoutSuccessUrl("/logout");
}
//内存中创建用户,用户名为pbm,密码123,权限是USER
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("pbm").password("123").roles("USER");
}
}
创建一个TestController负责跳转,代码如下:
package com.pbm.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 控制器
* @author Administrator
*
*/
@Controller
public class TestController {
@RequestMapping("/")
public String index(){
return "index";
}
@RequestMapping("/index")
public String index2(){
return "index";
}
@RequestMapping("/user")
public String user(){
return "user/index1";
}
@RequestMapping("/admin")
public String admin(){
return "admin/index2";
}
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/login_error")
public String login_error(Model model){
model.addAttribute("login_error", "用户名或密码错误");
return "login";
}
@RequestMapping("/logout")
public String logout(Model model){
model.addAttribute("login_error", "注销成功");
return "login";
}
@RequestMapping("/401")
public String error(){
return "401";
}
}
(1)创建一个user/index1.html,用于校验USER权限,没有登录的话不能直接访问,代码如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>user/index</title>
</head>
<body>
user/index
用于校验USER权限,没有登录的话不能直接访问
<form th:action="@{/logout}" method="post">
<input type="submit" value="注销"/>
</form>
</body>
</html>
(2)创建一个admin/index2.html,只允许ADMIN权限访问,代码如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
index
只允许ADMIN权限访问
</body>
</html>
(3)401页面,用于没有权限跳转:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>401</title>
</head>
<body>
401页面,用于没有权限跳转
</body>
</html>
(4)index页面,任何权限都能访问
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
index页面,任何权限都能访问
</body>
</html>
(5)login页面,用于登录
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>登陆页面</title>
</head>
<body>
<h1>登陆页面</h1>
<form th:action="@{/login}" method="post">
<span th:text="${login_error}"></span>
<br/>
<input type="text" name="username">用户名<br/>
<input type="password" name="password">密码<br/>
<input type="submit" value="登录">
</form>
</body>
</html>
到这里就全部创建完成了,启动项目,访问http://localhost:8080/,如图,可以直接访问。
访问http://localhost:8080/user被拦截到http://localhost:8080/login,如图
先输入错误的密码,如图
然后输入用户名pbm密码123,点击登录结果如图
访问http://localhost:8080/admin,如图,没有权限
我们在回到http://localhost:8080/user点击注销,如图
springboot整合SpringSecurity
百度网盘链接:https://pan.baidu.com/s/1o8zowoQUbiYPgqCVnFOLiw
提取码:a3nz