体系的组织结构设计(重要组件、模块划分、模块间交互)
常用工具
一、需求调研、需求分析(即应用场景)
- spring security 是基于 spring 的安全框架,它提供全面的安全性解决方案。
二、框架的设计思想
- 认证(你是谁,用户/设备/系统,是否合法)。
- 验证(你有干什么的权限,也叫权限控制/授权,允许执行的操作)。ds:这个验证可以控制到方法的级别,验证用户能做什么。
三、工作原理、运行流程
文: 基于 Filter , Servlet, AOP 实现身份认证和权限验证。
四、详细设计:实现方法(技术)
- spring security同时在Web请求级和方法调用级处理身份确认和授权。
- spring security使用spring的IOC、AOP为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复 代码的工作。
ds:aop做的是在你调用方法之前呢,使用面向切面编程实现验证功能。
ds:引入的spring-security-web.jar包实现在web中实现验证的管理。
五、使用说明:常用配置
1、Using generated security password: 79e0d51a-03cf-4d46-abdd-6ba4c79d342c
ds:当前在初步使用时, spring security给你生成的临时密码。用户是:user,密码是:79e0d51a-03cf-4d46-abdd-6ba4c79d342c,你现在使用user + 79e0d51a-03cf-4d46-abdd-6ba4c79d342c是可以访问系统的。
2、关闭验证
3、java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
spring security 5 版本要求密码比较加密,否则报错
六、具体1:例子1:临时、配置式的用户名和密码
1、默认用户名+临时密码
注:如果消除浏览器缓存后再访问XxxController.java,还是要登录。
ds:这个login登录窗口是spring security默认帮我们实现的登录窗口,只有用户名和密码正确才能访问XxxController.java中的方法。
实现原理是,spring security在处理器XxxController.java中的方法的前面使用aop做了一个拦截,这个拦截使用的就是spring security框架实现的身份认证,如果用户正确才能访问处理器的处理方法。
2、自定义用户名和密码
spring: security: user: name: wkcto password: wkcto
七、具体2:例子2:使用内存中的用户信息
package com.wkcto.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
// 1. @Configuration :表示当前类是一个配置类(相当于是 spring 的 xml 配置文件),在这个类方法的返回值是 java 对象,这些对象放入到 spring 容器中。
// 2. @EnableWebSecurity:表示启用 spring security 安全框架的功能
// 3. @Bean:把方法返回值的对象,放入到 spring 容器中。
@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder pe = passwordEncoder();
auth.inMemoryAuthentication().withUser("zhansan").password(pe.encode("3")).roles();
auth.inMemoryAuthentication().withUser("lisi").password(pe.encode("4")).roles();
auth.inMemoryAuthentication().withUser("wangwu").password(pe.encode("5")).roles();
}
//创建 PasawordEncoder 的实现类, 实现类是加密算法
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
八、具体3:例子3:基于(内存中)角色 Role 的身份认证, 同一个用户可以有不同的角色。同时可以开启对方法级别的认证(ds:用户访问方法时,在方法之上提供认证功能;)。
1、第一步:设置用户的角色
ds:继承WebSecurityConfigurerAdapter.java,可以实现/提供自定义的认证信息的配置类。我们通过覆盖相应的方法,来提供自定义的,关于用户呀、认证呀,角色等等配置信息。
//定义两个角色 normal(普通), admin(超级管理员)
auth.inMemoryAuthentication()
.withUser("zhangsan")
.password(pe.encode("123456"))
.roles("normal");
auth.inMemoryAuthentication()
.withUser("lisi")
.password(pe.encode("123456"))
.roles("normal");
auth.inMemoryAuthentication()
.withUser("admin")
.password(pe.encode("admin"))
.roles("admin","normal");
2、第二步:告诉框架,启动角色的功能
/**
* @EnableGlobalMethodSecurity:启用方法级别的认证,即认证这个方法能不能被访问
* prePostEnabled:boolean 默认是false
* true:表示可以使用@PreAuthorize注解 和 @PostAuthorize
* @PreAuthorize:放在方法之上,给方法提供认证功能,当前用户属于指定角色,认证才能成功才能执行方法
* @PostAuthorize:放在方法之上,给方法提供认证功能,当前用户属于指定角色,认证才能成功才能执行方法
*/
@EnableGlobalMethodSecurity(prePostEnabled = true)
完整版本:
package com.wkcto.config;
import org.springframework.context.annotation.Bean;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @EnableGlobalMethodSecurity:启用方法级别的认证
* prePostEnabled:boolean 默认是false
* true:表示可以使用@PreAuthorize注解 和 @PostAuthorize
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
//在方法中配置 用户和密码的信息, 作为登录的数据
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder pe = passwordEncoder();
//定义两个角色 normal(普通), admin(超级管理员)
auth.inMemoryAuthentication()
.withUser("zhangsan")
.password(pe.encode("123456"))
.roles("normal");
auth.inMemoryAuthentication()
.withUser("lisi")
.password(pe.encode("123456"))
.roles("normal");
auth.inMemoryAuthentication()
.withUser("admin")
.password(pe.encode("admin"))
.roles("admin","normal");
}
//创建密码的加密类
@Bean
public PasswordEncoder passwordEncoder(){
//创建PasawordEncoder的实现类, 实现类是加密算法
return new BCryptPasswordEncoder();
}
}
3、第三步:设置角色与方法的对应关系
package com.wkcto.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String sayHello(){
return "使用内存中的用户信息";
}
//指定 normal 和admin 角色都可以访问的方法
@RequestMapping("/helloUser")
@PreAuthorize(value = "hasAnyRole('admin','normal')")
public String helloCommonUser(){
return "Hello 拥有normal, admin角色的用户";
}
//指定admin角色的访问方法
@RequestMapping("/helloAdmin")
@PreAuthorize("hasAnyRole('admin')")
public String helloAdmin(){
return "Hello admin角色的用户可以访问";
}
}
4、第四步:测试说明
(1)第1步:访问:http://localhost:8080/helloUser,跳转到登录页面:http://localhost:8080/login
(2)第2步:普通用户(normal)和超级管理员(admin)都可以访问helloUser方法
(3)第3步:清除缓存(cookie等其它数据)
(4)第4步:只有超级管理员(admin)都可以访问helloAdmin方法
九、具体3:例子3:基于 jdbc 的用户认证。
1、第一步:有spring security 框架UserDetails接口
在 spring security 框架对象中用户信息的表示类是 UserDetails。UserDetails 是一个接口,高度抽象的用户信息类(相当于项目中的 User 类)。
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public interface UserDetails extends Serializable {
// 用户的角色信息列表,是一个集合,因为一个用户是可以有多个角色的
Collection<? extends GrantedAuthority> getAuthorities();
// 获取用户名
String getPassword();
// 获取密码
String getUsername();
// 账号是否是过期的
boolean isAccountNonExpired();
// 账号是否是锁定的
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
// 账号是否是启用的
boolean isEnabled();
}
2、第二步:有spring security 框架User类实现了UserDetails接口
ds:我们可以创建User.java类的一个对象,来代表用来认证的用户信息,用来提供身价认证的数据。 spring security 框架底层会调用User.java对象,来进行用户身份的认证的。
因此,我们需要做的是去创建这个User.java对象,封装此用户的:用户名、密码、角色集合,这3个用户信息就可以了。
文档:需要向 spring security 提供 User 对象, 这个对象的数据来自数据库的查询。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.core.userdetails;
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = 500L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
// username:用户名
// password:密码
// authorities:角色的集合
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
if (username != null && !"".equals(username) && password != null) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}
......
}
3、第三步:实现 UserDetailsService 接口(怎么获取到User对象?怎么去查询数据库呢?)
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
// 参数:var1:传递参数userName
// 返回值:UserDetails,security框架就是用这个对象来认证用户的。
/*
* 我们可以在loadUserByUsername()中去访问数据库,来获取到我们需要的用户信息。
*/
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
重写方法 UserDetails loadUserByUsername(String var1) 在方法中获取数据库中的用户信息, 也就是执行数据库的查询,条件是用户名称。
ds:Spring Security框架会自动检测到我们MyUserDetailService.java实现了UserDetailsService 接口,因此Spring Security框架底层就会自动调用loadUserByUsername()方法,其中会调用dao获取数据库中用户的数据信息(用户名、密码、角色),并返回UserDetails接口的实现User.java对象。由User对象提供了,用户名、密码、角色这些认证信息。
package com.wkcto.provider;
import com.wkcto.dao.UserInfoDao;
import com.wkcto.entity.UserInfo;
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.Component;
import java.util.ArrayList;
import java.util.List;
@Component("MyUserDetailService")
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserInfoDao dao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = null;
UserInfo userinfo = null;
if( username != null){
userinfo = dao.findByUsername(username);
if( userinfo != null){
List<GrantedAuthority> list = new ArrayList<>();
//角色必须以ROLE_开头(规则:必须有此前缀)
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" +userinfo.getRole());
list.add(authority);
//创建User对象
user = new User(userinfo.getUsername(),userinfo.getPassword(),list);
}
}
return user;
}
}
4、第四步:重写自定义配置类MyWebSecurityConfig.java
package com.wkcto.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("MyUserDetailService")
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth:验证器
// auth.userDetailsService()方法需要传入一个UserDetailsService的实例做为参数。
// .passwordEncoder( new BCryptPasswordEncoder())表明使用的密码加密方式是BCryptPasswordEncoder,这个也是我们常用的方式
auth.userDetailsService(userDetailsService).passwordEncoder( new BCryptPasswordEncoder());
}
}
5、第五步:XxxControler类
package com.wkcto.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String sayHello(){
return "使用内存中的用户信息";
}
//指定 normal 和admin 角色都可以访问的方法
@RequestMapping("/helloUser")
@PreAuthorize(value = "hasAnyRole('admin','normal')")
public String helloCommonUser(){
return "===Hello 拥有normal, admin角色的用户===";
}
//指定admin角色的访问方法
@RequestMapping("/helloAdmin")
@PreAuthorize("hasAnyRole('admin')")
public String helloAdmin(){
return "===Hello admin角色的用户可以访问==";
}
}
6、第六步:测试
附1、pom.xml文件
<?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.wkcto</groupId>
<artifactId>ch04-jdbc</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<!--加入spring boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<!--指定依赖-->
<dependencies>
<!--web开发相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring security项目的起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.9</version>
</dependency>
<!--数据库访问框架jpa-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
附2、UserInfo.java用户信息表
package com.wkcto.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
// jpa中定义的一个注解
// 表示当前类是一个实体类, 表示数据库中的一个表
// 表名默认和类名一样的
@Entity
public class UserInfo {
// 用户id
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 用户名称
private String username;
// 密码
private String password;
// 角色
private String role;
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 getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
附3、dao层
package com.wkcto.dao;
import com.wkcto.entity.UserInfo;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* JpaRepository<UserInfo,Long>
* 泛型UserInfo:用户信息实体类
* Long:主键是Long类型的
*
* UserInfoDao继承了JpaRepository,这样我们就有一个访问数据库的这么一个类了,叫做UserInfoDao
*/
public interface UserInfoDao extends JpaRepository<UserInfo,Long> {
//按照username查询数据库信息
UserInfo findByUsername(String username);
}
附4、service层
package com.wkcto.service;
import com.wkcto.entity.UserInfo;
public interface UserInfoService {
UserInfo findUserInfo(String username);
}
package com.wkcto.service.impl;
import com.wkcto.dao.UserInfoDao;
import com.wkcto.entity.UserInfo;
import com.wkcto.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Autowired
private UserInfoDao dao;
@Override
public UserInfo findUserInfo(String username) {
UserInfo userinfo = dao.findByUsername(username);
return userinfo;
}
}
附5、application.properties
spring.datasource.url=jdbc:mysql:///mysql?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# DDL生成
spring.jpa.generate-ddl=true
# 显示sql
spring.jpa.show-sql=true
# 使用的数据库mysql
spring.jpa.database=mysql
附6、主启动类JdbcApplication.java
package com.wkcto;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JdbcApplication {
public static void main(String[] args) {
SpringApplication.run(JdbcApplication.class,args);
}
}
附7、数据库初始化类JdbcInit.java
package com.wkcto.init;
import com.wkcto.dao.UserInfoDao;
import com.wkcto.entity.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
// 注入到容器中(数据库初始化完成后注释掉)
@Component
public class JdbcInit {
// 注入dao
@Autowired
private UserInfoDao dao;
// 对象创建之后呢,来执行这个方法(数据库初始化完成后注释掉)
@PostConstruct
public void init(){
// spring security加密功能
PasswordEncoder encoder = new BCryptPasswordEncoder();
UserInfo u = new UserInfo();
u.setUsername("zhangsan");
u.setPassword(encoder.encode("123456"));
u.setRole("normal");
dao.save(u);
u = new UserInfo();
u.setUsername("admin");
u.setPassword(encoder.encode("admin"));
u.setRole("admin");
dao.save(u);
}
}
初始化数据效果,如下图:
十、其它具体
1、Spring Security与Spring的关系
ds:Spring Security是完全基于spring框架的。在 Spring Framework 基础 上,spring security 充分利用了依赖注入(DI)和面向切面编程(AOP)功能,为 应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
2、Spring Security与Spring MVC的关系
文:是一个轻量级的安全框架。它与 Spring MVC 有很好地集成。
3、Spring Security是否可以单独使用?
ds:Spring Security也可以单独使用,来完成身份认证以及权限认证功能。
4、概念:认证与授权
- authentication:认证, 认证访问者是谁。 一个用户或者一个其他系统是不是当前要访问的系统中的有效用户。认证完成以后,仅确定此用户/系统是有效用户,但是它具体能干什么,还要看授权情况。
- authorization:授权, 访问者能做什么,对系统中的资源能做哪些操作(复制、粘贴、访问Controller的方法等)
5、RBAC模型
ds:在做认证和授权中,我们主要采用的模型是RBAC模型。RBAC模型是在做权限控制中,应用得非常广泛的模型。
- 系统中有张三,李四,他们是普通员工,只能查看数据。
- 系统中经理,副经理他们能修改数据。
设计过程:
第一步:设计有权限的集合:
经理角色,具有修改数据的权限,删除,查看等等。
公司增加经理了, 只要加入到“经理角色”就可以了。
RBAC 设计中的表:
- 用户表: 用户认证(登录用到的表),包含字段一般为:用户名,密码,是否启用,是否锁定等信息。
- 角色表:定义角色信息,包含字段一般是:角色名称, 角色的描述。
- 用户和角色的关系表: 用户和角色是多对多的关系。 一个用户可以有多个角色, 一个角色可以有多个用户。
- 权限表, 用于存储角色可以有哪些权限。
- 角色和权限的关系表,属于有业务需求时扩展的表。
ds:RBAC权限控制功能模块,应该是像AOP的切面一样,可以单独存在的,可以反复使用,即可以像切面一样切入需要做权限控制的地方。可以使用在多个系统中,可以单独进行维护和管理的。
而spring security框架,就是可以独立开于我们的系统,来进行权限的控制和管理的框架(系统)。