之前我们对Shiro进行了了解,但是很多东西都是和Spring Security结合的,为了后续学习其他内容的方便, 我们在这里讲解下Spring自己的安全机制,Spring Security。
什么是Spring Security
Spring Security是专门针对基于Spring的项目的安全框架,充分的利用了依赖注入和AOP来实现安全功能。
安全框架有两个重要的概念,即认证(Authentication)和授权(Authorization)。认证就是确认用户可以访问当前系统;授权就是确定用户在当前系统下拥有的权限。
注意:
我们这里采取的是Spring Boot + Mybatis + Mysql + Spring Security进行构建演示的。
一,数据库数据的导入
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `sys_user_role` (
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`user_id`,`role_id`),
KEY `fk_role_id` (`role_id`),
CONSTRAINT `fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `sys_role` VALUES ('1', 'ROLE_ADMIN');
INSERT INTO `sys_role` VALUES ('2', 'ROLE_USER');
INSERT INTO `sys_user` VALUES ('1', 'admin', '123');
INSERT INTO `sys_user` VALUES ('2', 'zyf', '123');
INSERT INTO `sys_user_role` VALUES ('1', '1');
INSERT INTO `sys_user_role` VALUES ('2', '2');
一般我们的权限控制分为三层
用户 角色 权限
其中三者中角色和权限是多对多,用户和角色是多对多。
这里我们的实验只考虑角色和用户,暂时不考虑权限
二, 程序:
- 基础架构:
- 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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xiyou</groupId>
<artifactId>test-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test-security</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- application.xml
server.port=8080
#描述数据源
spring.datasource.url=jdbc:mysql://localhost:3306/security?useSSL=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=05131004
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#开启Mybatis下划线命名转驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
# 自动映射map的时候 若值是null,则可以映射出具体的key
mybatis.configuration.call-setters-on-nulls=true
- PO类
(1)SysRole
package com.xiyou.testsecurity.po;
import lombok.Data;
import java.io.Serializable;
/**
* 对应数据的角色表
*/
@Data
public class SysRole implements Serializable {
static final long serialVersionUID = 1L;
private Integer id;
private String name;
}
(2)SysUser
package com.xiyou.testsecurity.po;
import lombok.Data;
import java.io.Serializable;
/**
* 对应数据库的用户表
*/
@Data
public class SysUser implements Serializable {
static final long serialVersionUID = 1L;
private Integer id;
private String name;
private String password;
}
(3)SysUserRole
package com.xiyou.testsecurity.po;
import lombok.Data;
import java.io.Serializable;
/**
* 对应数据库的角色和权限表
*/
@Data
public class SysUserRole implements Serializable{
static final long serialVersionUID = 1L;
private Integer userId;
private Integer roleId;
}
- dao层
(1)SysRoleMapper
package com.xiyou.testsecurity.dao;
import com.xiyou.testsecurity.po.SysRole;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface SysRoleMapper {
@Select("SELECT * FROM sys_role WHERE id = #{id}")
SysRole selectById(Integer id);
}
(2)SysUserMapper
package com.xiyou.testsecurity.dao;
import com.xiyou.testsecurity.po.SysUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface SysUserMapper {
@Select("SELECT * FROM sys_user WHERE id = #{id}")
SysUser selectById(Integer id);
@Select("SELECT * FROM sys_user WHERE name = #{name}")
SysUser selectByName(String name);
}
(3)SysUserRoleMapper
package com.xiyou.testsecurity.dao;
import com.xiyou.testsecurity.po.SysUserRole;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface SysUserRoleMapper {
@Select("SELECT * FROM sys_user_role WHERE user_id = #{userId}")
List<SysUserRole> listByUserId(Integer userId);
}
- Service层
(1)SysRoleService
package com.xiyou.testsecurity.service;
import com.xiyou.testsecurity.dao.SysRoleMapper;
import com.xiyou.testsecurity.po.SysRole;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SysRoleService {
@Autowired
private SysRoleMapper roleMapper;
public SysRole selectById(Integer id){
return roleMapper.selectById(id);
}
}
(2)SysUserRoleService
package com.xiyou.testsecurity.service;
import com.xiyou.testsecurity.dao.SysUserRoleMapper;
import com.xiyou.testsecurity.po.SysUserRole;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SysUserRoleService {
@Autowired
private SysUserRoleMapper userRoleMapper;
public List<SysUserRole> listByUserId(Integer userId) {
return userRoleMapper.listByUserId(userId);
}
}
(3)SysUserService
package com.xiyou.testsecurity.service;
import com.xiyou.testsecurity.dao.SysUserMapper;
import com.xiyou.testsecurity.po.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SysUserService {
@Autowired
private SysUserMapper userMapper;
public SysUser selectById(Integer id) {
return userMapper.selectById(id);
}
public SysUser selectByName(String name) {
return userMapper.selectByName(name);
}
}
- Controller层
package com.xiyou.testsecurity.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@Controller
public class HelloController {
@ResponseBody
@RequestMapping("/sayHello")
public String sayHello(){
return "Hello ~ ";
}
@ResponseBody
@RequestMapping("/showUser")
public String showUsere(){
// 获取当前登录者的名字
String name = SecurityContextHolder.getContext().getAuthentication().getName();
return name;
}
@RequestMapping("/login")
public String login(){
return "login.html";
}
@ResponseBody
@RequestMapping("/admin")
// 访问该方法需要admin权限
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String printAdmin(){
return "This is Admin Page";
}
@ResponseBody
@RequestMapping("/user")
// 访问该方法需要user权限
@PreAuthorize("hasRole('ROLE_USER')")
public String printUser(){
return "This is user page";
}
}
这里简单的介绍下这个Controller层。
(1)看下这里的login方法,该方法返回的是一个html页面,因为我们并未标注@ResponseBody注解,返回的是一个html页面,该页面存放在resources/static目录下。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<h1>登陆</h1>
<form method="post" action="/login">
<div>
用户名:<input type="text" name="username">
</div>
<div>
密码:<input type="password" name="password">
</div>
<div>
<button type="submit">立即登陆</button>
</div>
</form>
</body>
</html>
(2)showUser方法中
String name = SecurityContextHolder.getContext().getAuthentication().getName();
这行代码的意思是获取当前登录者的名字,获取当前登陆者的信息。
(3)注解@PreAuthorize
@PreAuthorize("hasRole('ROLE_USER')")
这个注解的意思是若想执行当前的方法,必须要满足当前的登录用户角色是ROLE_USER , 若不满足则无法正常执行这个方法。
- 启动函数
package com.xiyou.testsecurity;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(value = {"com.xiyou.testsecurity.dao"})
public class TestSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(TestSecurityApplication.class, args);
}
}
通过上面的程序我们基本上就已经搭建出了一个具体的环境提供我们实验Spring Security,我们之后的实验都是在这个基础上进行的。
三,配置SpringSecurity(重要)
我们这里进行的配置是直接从我们的数据源中获取用户信息,还有两种获取方式,这里不做讲解,分别是内存中的用户和指定JDBC中的用户。
我们若想实现自己的数据源来对接Spring Security则必须自定义UserDetailsService接口
package com.xiyou.testsecurity.service;
import com.xiyou.testsecurity.po.SysRole;
import com.xiyou.testsecurity.po.SysUser;
import com.xiyou.testsecurity.po.SysUserRole;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 验证的方法,需要实现指定的接口
* 重写该方法
*/
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
// 注入我们自己实现的service层的相关信息
@Autowired
private SysRoleService sysRoleService;
@Autowired
private SysUserService sysUserService;
@Autowired
private SysUserRoleService sysUserRoleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 用来存储该username对应的所有的角色
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 根据传来得到username, 从数据库中取出sys_user对象
SysUser sysUser = sysUserService.selectByName(username);
// 判断该user是否为空
if(sysUser == null){
throw new UsernameNotFoundException("该用户不存在");
}
// 为该用户添加权限
// 得到sysuser对应的所有的sysrole的id列表
List<SysUserRole> userRoles = sysUserRoleService.listByUserId(sysUser.getId());
for(SysUserRole sysUserRole : userRoles){
SysRole sysRole = sysRoleService.selectById(sysUserRole.getRoleId());
// 添加到最先定义的列表中
authorities.add(new SimpleGrantedAuthority(sysRole.getName()));
}
// 返回UserDetails的实现类
// 该User类是security定义的,将当前用户的用户名密码,角色传入进去
return new User(sysUser.getName(), sysUser.getPassword(), authorities);
}
}
- 分析上面的程序
(1)该类实现了UserDetailsService接口
(2)重写loadUserByUsername方法,该方法的参数是一个String类型的,默认会将客户登录的用户名传进来
(3)定义一个List,其泛型是GrantedAuthority
Collection<GrantedAuthority> authorities = new ArrayList<>();
该list是用来存储当前用户所对应的所有的角色名。
(4)我们首先会根据传进来的名字对数据库进行查找,若数据库中没有该对象则抛出异常。
(5)若数据库中有该name对应的对象,则从数据库中取出该对象对应的所有的Role的信息。
(6)将其role的name进行封装,并保存在上面的list中
// 添加到最先定义的列表中
authorities.add(new SimpleGrantedAuthority(sysRole.getName()));
(7)返回一个User对象,注意该对象是Security的对象,该对象有三个参数,分别是当前用户的name,password,以及封装好的authorities列表,存储着其对应的角色信息。
这里我们只是根据name属性进行匹对,有的人可能有疑惑,那密码不正确怎么办呢?其实我们这里可以用username和password共同查找或者在这里进行判断密码是否正确,但是我们也可以在配置类中进行匹对。下面我们就会对其进行分析
代码如下:
package com.xiyou.testsecurity.config;
import com.xiyou.testsecurity.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
// 开启安全的注解形式,默认是关闭的
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
/**
* 定制登陆行为
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login")
.defaultSuccessUrl("/sayHello").permitAll()
.and()
.logout().permitAll();
// 关闭CSRF
http.csrf().disable();
}
/**
* 该方法用来校验
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// boot2.0版本之后需要添加passwordEncoder否则会报错的。
auth.userDetailsService(userDetailsService).passwordEncoder(
new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
/**
*
* @param charSequence 用户登录时候传入的密码
* @param s 数据库中的密码,如果数据库没有该对象则赋值为"userNotFoundPassword"
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
// s是数据库中的密码,charSequence是用户输入的密码,如果返回是false,依旧不会登录成功,返回true才可以。
return s.equals(charSequence.toString());
}
}
);
}
/**
* 设置过滤静态资源
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**");
}
/**
* @Override
* 如果密码要加密的话
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
*/
}
- 该类上面有三个注解
(1)@Configuration:表示的是该类是一个类似xml文件的配置类
(2)@EnableWebSecurity表示开启Security服务
(3)@EnableGlobalMethodSecurity表示对是开启Security的注解,默认是关闭的,只有开启才能使用类似@PreAuthorize(“hasRole(‘ROLE_USER’)”)的注解 - 继承WebSecurityConfigureAdapter类,继承该类用来配置相关的操作。
该类重写了三个方法:
(1)protected void configure(HttpSecurity http)
该方法用来定制登陆行为
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login")
.defaultSuccessUrl("/sayHello").permitAll()
.and()
.logout().permitAll();
// 关闭CSRF
http.csrf().disable();
}
表示的是针对任意请求用户都需要进行登录,并且其登录页面是访问/login(controller层定义的),默认的成功action是/sayHello(controller层定义的)
注意:
这里强调几点
- http.authorizeRequests()用来配置请求权限,调用该方法来开始请求权限的配置
- Spring Security使用以下匹配器来匹配请求路径
antMatchers:使用Ant风格的路径匹配(类似于/admin/**这种的)
regexMatchers:使用正则表达式匹配路径
方法 | 用途 |
---|---|
anyRequest | 匹配所有的请求路径 |
access(String) | Spring EL表达式结果为true时可以访问 |
anonymous() | 匿名可以访问 |
denyAll() | 用户不能访问 |
fullyAuthenticated() | 用户完全认证可访问(非remember下自动登录) |
hasAnyAuthority(String…) | 如果用户有参数,则其中任一权限可以访问 |
hasAnyRole(String…) | 如果用户有参数,则其中任一角色可以访问 |
hasAuthority(String) | 如果用户有参数,则其权限可以访问 |
hasRole(String) | 若用户参数中的角色,可以访问 |
hasIpAddress(String) | 如果用户来自参数中的IP,则可以访问 |
permitAll() | 用户可以任意访问 |
rememberMe() | 允许通过remember-me登录的用户访问 |
authenticated() | 用户登录后可以访问 |
(2)protected void configure(AuthenticationManagerBuilder auth)
- 该方法用来做登陆校验
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// boot2.0版本之后需要添加passwordEncoder否则会报错的。
auth.userDetailsService(userDetailsService).passwordEncoder(
new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
/**
*
* @param charSequence 用户登录时候传入的密码
* @param s 数据库中的密码,如果数据库没有该对象则赋值为"userNotFoundPassword"
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
// s是数据库中的密码,charSequence是用户输入的密码,如果返回是false,依旧不会登录成功,返回true才可以。
return s.equals(charSequence.toString());
}
}
);
}
上面的方法,将我们自己写的CustomUserDetailsService的类的对象,注入进来。这里注意一点,因为我们使用的是springboot2所以我们在注入这个对象后,还应该定义passwordEncoder
- 这里重点看下上面的matches方法
/**
*
* @param charSequence 用户登录时候传入的密码
* @param s 数据库中的密码,如果数据库没有该对象则赋值为"userNotFoundPassword"
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
// s是数据库中的密码,charSequence是用户输入的密码,如果返回是false,依旧不会登录成功,返回true才可以。
return s.equals(charSequence.toString());
}
像我们注释中的一样,这里有两个参数
CharSequence 和 一个String ,其中charSequence 用户登录时候传入的密码,String参数就是数据库中的密码,如果数据库没有该对象则赋值为"userNotFoundPassword"
注意
我们之前只比较了用户名是否在数据库中存在,若存在直接将数据库中的用户名和密码以及角色封装成User对象进行返回,当时并没有比较密码是否相等,其实,我们的密码比较就在这里。若传入的密码和数据中的密码一致,返回true才能表示验证成功,否则不一致返回false表示的是验证不通过。
(3)public void configure(WebSecurity web)
该方法是用来过滤静态资源,使其可以正常访问,不用进行校验。
/**
* 设置过滤静态资源
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**");
}
声明一点:
我们在这里并没有对用户的密码进行加密,如果要进行加密操作的比较,则修改方法,改为:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
上面的实验结果就是: