spring security简介
什么是spring security
spring security 是基于 spring 的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。在 Spring Framework 基础上,spring security 充分利用了依赖注入(DI)和面向切面编程(AOP)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。是一个轻量级的安全框架。它与 Spring MVC 有很好地集成.
spring security 核心功能
(1)认证(你是谁,用户/设备/系统) (2)验证(你能干什么,也叫权限控制/授权,允许执行的操作)。
spring security 原理
基于 Filter , Servlet, AOP 实现身份认证和权限验证
spring security实例
初探spring security
- 创建 maven 项目
- 加入依赖:spring boot 依赖, spring security 依赖
<!--加入 spring boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<!--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>
- 创建 Controller,接收请求
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author tao
* @date 2021-04-04 9:45
* 概要:
*/
@Controller
@RequestMapping()
public class HelloSecurityController {
@RequestMapping("/test1")
@ResponseBody
public String sayHello() {
return "Hello Spring Secuirty 安全管理框架";
}
}
- 框架生成的用户 用户名: user 密码: 在启动项目时,生成的临时密码。uuid 日志中生成的密码 generated security password: 9717464c-fafd-47b3-9995-2c18b24f7336
- 自定义用户名和密码 需要在 springboot 配置文件中设置登录的用户名和密码 在 resource 目录下面创建 spring boot 配置文件 application.yml(application.properties) security: user: name: Nick password: Nick name:自定义用户名称 password:自定义密码
- 关闭验证 //排除Security的配置,让他不启用 @SpringBootApplication(exclude = {SecurityAutoConfiguration.class}) //@SpringBootApplication public class SercurityTest1Application { public static void main(String[] args) { SpringApplication.run(SercurityTest1Application.class, args); } }
使用内存中的用户信息
使用内存中的用户信息是指在服务器的内存中简单的配置用户信息,进行权限相应的配置,可以更灵活的进行功能测试,并没有使用数据库。 1)使用:WebSecurityConfigurerAdapter 控制安全管理的内容。 需要做的使用:继承 WebSecurityConfigurerAdapter,重写方法。实现 自定义的认证信息。重写下面的方法。 protected void configure(AuthenticationManagerBuilder auth) 2)spring security 5 版本要求密码进行加密,否则报错 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" 实现密码加密:
- 创建用来加密的实现类(选择一种加密的算法)
@Bean
public PasswordEncoder passwordEncoder(){
//创建 PasawordEncoder 的实现类, 实现类是加密算法
return new BCryptPasswordEncoder();
}
- 给每个密码加密
PasswordEncoder pe = passwordEncoder();
pe.encode("123456")
3)示例代码如下
代码语言:javascript复制package cn.kt.sercurity_test1.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;
/**
* @author tao
* @date 2021-04-04 10:39
* 概要:使用内存中的用户信息
*/
/*
* @EnableGlobalMethodSecurity:启用方法级别的认证
* prePostEnabled:默认是false
* true:表示可以使用@PreAuthorize 和 @PostAuthorize
*/
@Configuration
@EnableWebSecurity
public class MySecurityConfig {
//在方法中配置用户和密码的信息、作为登录信息
//定义两个角色 normal admin
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder pe = passwordEncoder();
auth.inMemoryAuthentication().withUser("admin").password(pe.encode("admin")).roles();
auth.inMemoryAuthentication().withUser("Nick").password(pe.encode("Nick")).roles();
auth.inMemoryAuthentication().withUser("Amy").password(pe.encode("Amy")).roles();
}
//创建密码的加密类
@Bean
public PasswordEncoder passwordEncoder() {
//创建PasswordEncoder的实现类,实现类是加密算法
return new BCryptPasswordEncoder();
}
}
注解: 1. @Configuration :表示当前类是一个配置类(相当于是 spring 的 xml 配置文件),在这个类方法的返回值是 java 对象,这些对象放入到 spring 容器中。 2. @EnableWebSecurity:表示启用 spring security 安全框架的功能 3. @Bean:把方法返回值的对象,放入到 spring 容器中。
基于角色 Role 的身份认证
基于角色的实现步骤:
1、设置用户的角色 继承 WebSecurityConfigurerAdapter 重写 configure 方法。指定用户的 roles auth.inMemoryAuthentication().withUser("admin").password(pe.encode("admin")).roles("admin","normal"); 2、在类的上面加入启用方法级别的注解 @EnableGlobalMethodSecurity(prePostEnabled = true)
代码语言:javascript复制@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
}
加上@EnableGlobalMethodSecurity(prePostEnabled = true)注解之后在方法层面,就可以手动使用使用@PreAuthorize 指定在方法之前进行角色的认证,指定方法可以访问的角色列表。 如:hasAnyRole('角色名称 1','角色名称 N');
代码语言:javascript复制 package cn.kt.sercurity_test1.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author tao
* @date 2021-04-04 11:10
* 概要:
*/
@RestController
public class AuthorizeSecurityController {
//指定 normal和admin都可以访问的通用的方法
@RequestMapping("/testUser")
@PreAuthorize(value = "hasAnyRole('admin','normal')")
public String testUser() {
return "Hello Spring Secuirty normal和admin都可以访问的通用的方法";
}
//指定 admin访问的专有的方法
@RequestMapping("/testAdmin")
@PreAuthorize(value = "hasAnyRole('admin')")
public String testAdmin() {
return "Hello Spring Secuirty admin访问的专有的方法";
}
}
3、或者在配置类中,进行权限的统一配置 如以下编写的配置类:
代码语言:javascript复制package cn.kt.securitytest2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
// @EnableWebSecurity已经自动导入了配置注解@Configuration,可以不不用重复添加
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
//定制请求的授权规则
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("VIP1")
.antMatchers("/level2/**").hasRole("VIP2")
.antMatchers("/level3/**").hasRole("VIP3");
//开启自动配置的登陆功能,效果,如果没有登陆,没有权限就会来到登陆页面
http.formLogin().usernameParameter("user").passwordParameter("pwd")
.loginPage("/userlogin");
//1、/login来到登陆页
//2、重定向到/login?error表示登陆失败
//3、更多详细规定
//4、默认post形式的 /login代表处理登陆,
//5、自定义登录后,默认POST的账号为username参数,密码而password参数,可以通过usernameParameter()和passwordParameter()自定义修改
//6、一但定制loginPage;那么 loginPage的post请求就是登陆
//开启自动配置的注销功能。
http.logout().logoutSuccessUrl("/");//注销成功以后来到首页
//1、访问 /logout 表示用户注销,清空session
//2、注销成功会返回 /login?logout 页面;
//开启记住我功能
http.rememberMe().rememberMeParameter("remeber");
//登陆成功以后,将cookie发给浏览器保存,以后访问页面带上这个cookie,只要通过检查就可以免登录
//点击注销会删除cookie
//自已的登陆页面记住我功能需要传递一个remember-me的布尔型参数判断是否记住登录,可以通过rememberMeParameter()来自定义记住我传递的参数名
// 静态资源等等不需要认证
http.authorizeRequests().antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/webSocket/**"
).permitAll();
}
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//super.configure(auth);
PasswordEncoder pe = passwordEncoder();
auth.inMemoryAuthentication()
.withUser("zhangsan").password(pe.encode("123456")).roles("VIP1", "VIP2")
.and()
.withUser("lisi").password(pe.encode("123456")).roles("VIP2", "VIP3")
.and()
.withUser("wangwu").password(pe.encode("123456")).roles("VIP1", "VIP3");
}
//创建密码的加密类
@Bean
public PasswordEncoder passwordEncoder() {
//创建 PasawordEncoder 的实现类, 实现类是加密算法
return new BCryptPasswordEncoder();
}
}
注意: 1. 重写protected void configure(HttpSecurity http) throws Exception {}方法进行权限的授权控制 2. http.authorizeRequests().antMatchers("/").permitAll()表示所有人可以访问”/“首页;http.authorizeRequests().antMatchers("/level1/").hasRole("VIP1")表示只有有"VIP1"权限的人,才可以访问”/level1/xxx"页面下的内容。相应的很有必要添加静态资源不需要授权。** 3. http.formLogin()开启自动配置的登陆功能,效果:如果没有登陆,没有权限就会来到登陆页面,相应的也可以按照代码中的注释来自定义登录规则 4. http.logout().logoutSuccessUrl("/")开启自动配置的注销功能,注销成功以后来到首页,并且清空session 5. http.rememberMe()开启记住我功能,会将cookie发给浏览器保存,以后访问页面带上这个cookie,只要通过检查就可以免登录
基于数据库认证
集成步骤
1、 数据库脚本
代码语言:javascript复制DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`nameEN` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色英文名称',
`nameZh` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色中文名称',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
INSERT INTO `role` VALUES (1, 'VIP1', 'I类VIP');
INSERT INTO `role` VALUES (2, 'VIP2', 'II类VIP');
INSERT INTO `role` VALUES (3, 'VIP3', 'III类VIP');
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户密码',
`enabled` tinyint(1) NULL DEFAULT NULL COMMENT '是否启用',
`locked` tinyint(1) NULL DEFAULT NULL COMMENT '是否被锁定',
`expired` tinyint(1) NULL DEFAULT NULL COMMENT '账户是否过期',
`credentialsExpire` tinyint(1) NULL DEFAULT NULL COMMENT '凭据是否过期',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
INSERT INTO `user` VALUES (1, 'zhangsan', '$2a$10$Ch8lK9.urrzsihbr3y9.ruEeWFe0s35JabDkhZt12q.x2bmrIAK7S', 1, 0, 0, 0);
INSERT INTO `user` VALUES (2, 'lisi', '$2a$10$NhYY27nG1V8LW4qLW5jfje/MwYpU/f6inh7X5rt/KJmJ1/tSUI9Nm', 1, 0, 0, 0);
INSERT INTO `user` VALUES (3, 'wangwu', '$2a$10$Z.YoIEK/Y1LTKOBBcI0U8Os/3nwe3gYsbRjoJnAiNIqbygIalYw2O', 1, 0, 0, 0);
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`uid` int(11) NULL DEFAULT NULL COMMENT '用户id',
`rid` int(11) NULL DEFAULT NULL COMMENT '角色id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 1, 2);
INSERT INTO `user_role` VALUES (3, 2, 2);
INSERT INTO `user_role` VALUES (4, 2, 3);
INSERT INTO `user_role` VALUES (5, 3, 3);
INSERT INTO `user_role` VALUES (6, 3, 1);
密码明文都为123。 配置数据库
代码语言:javascript复制 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/securitydb?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
2、实体类 User.java
代码语言:javascript复制package cn.kt.securitytest2.domin;
/**
* Created by tao.
* Date: 2021/10/27 10:39
* 描述:
*/
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.io.Serializable;
import java.util.*;
@Entity
@Getter
@Setter
@Table(name = "user")
public class User implements UserDetails, Serializable {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
@Column(name = "enabled")
private Boolean enabled;
@Column(name = "locked")
private Boolean locked;
@Column(name = "expired")
private Boolean expired;
@Column(name = "credentialsexpire")
private Boolean credentialsExpire;
@ManyToMany(targetEntity = Role.class, fetch = FetchType.EAGER, cascade = CascadeType.MERGE)
@JoinTable(name = "user_role",
//joinColumns配置当前对象在中间表中的外键(第一个参数是中间表的字段,第二个参数是本表对应的字段)
joinColumns = {@JoinColumn(name = "uid", referencedColumnName = "id")},
//inverseJoinColumns配置对方对象在中间表中的外键
inverseJoinColumns = {@JoinColumn(name = "rid", referencedColumnName = "id")}
)
private Set<Role> roles = new HashSet<Role>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { //返回用户的所有角色
List<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getNameEN()));
}
return authorities;
}
@Override
public boolean isAccountNonExpired() { //账户是否未过期
return expired;
}
@Override
public boolean isAccountNonLocked() { //账户是否未锁定
return locked;
}
@Override
public boolean isCredentialsNonExpired() { //凭证是否未过期
return credentialsExpire;
}
@Override
public boolean isEnabled() { //账户是否可用
return enabled;
}
@Override
public String toString() {
return "User{"
"id=" id
", username='" username '''
", password='" password '''
", enabled=" enabled
", locked=" locked
", expired=" expired
", credentialsExpire=" credentialsExpire
", roles=" roles
'}';
}
}
Role.java
代码语言:javascript复制package cn.kt.securitytest2.domin;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
/**
* Created by tao.
* Date: 2021/10/27 10:39
* 描述:
*/
@Entity
@Getter
@Setter
@Table(name = "role")
public class Role implements Serializable {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "nameen")
private String nameEN;
@Column(name = "namezh")
private String nameZh;
//多对多关系映射
@ManyToMany(mappedBy = "roles", fetch = FetchType.EAGER)
@JsonIgnore
private Set<User> users = new HashSet<User>();
@Override
public String toString() {
return "Role{"
"id=" id
", nameEN='" nameEN '''
", nameZh='" nameZh '''
'}';
}
}
这里使用的JPA的的多对多映射关系,这里可以看自己的习惯,不一定要使用多对多。
3、security配置文件 基于数据库认证的配置文件中,和基于内存数据最大的不同是User和Role都来源于数据库。 因此配置文件的配置也是如此。
代码语言:javascript复制@Autowired
UserSerivice userService;
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//super.configure(auth);
/*PasswordEncoder pe = passwordEncoder();
auth.inMemoryAuthentication()
.withUser("zhangsan").password(pe.encode("123456")).roles("VIP1", "VIP2")
.and()
.withUser("lisi").password(pe.encode("123456")).roles("VIP2", "VIP3")
.and()
.withUser("wangwu").password(pe.encode("123456")).roles("VIP1", "VIP3");*/
auth.userDetailsService(userService);
}
//创建密码的加密类
@Bean
public PasswordEncoder passwordEncoder() {
//创建 PasawordEncoder 的实现类, 实现类是加密算法
return new BCryptPasswordEncoder();
}
4、编写dao层 UserRepository.java
代码语言:javascript复制package cn.kt.securitytest2.repository;
import cn.kt.securitytest2.domin.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
* Created by tao.
* Date: 2021/10/27 13:30
* 描述:
*/
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
public User findByUsername(String username);
}
- 编写实现了UserDetailsService接口的Sercie层 UserService.java
package cn.kt.securitytest2.service;
import cn.kt.securitytest2.domin.Role;
import cn.kt.securitytest2.domin.User;
import cn.kt.securitytest2.repository.UserRepository;
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.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.List;
/**
* Created by tao.
* Date: 2021/10/27 14:29
* 描述:
*/
@Service
public class UserSerivice implements UserDetailsService {
@Autowired
private UserRepository userRepository;
/**
* 根据用户名查询用户信息
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
org.springframework.security.core.userdetails.User userdetail = null;
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
if (user.getRoles().size() != 0) {
List<GrantedAuthority> list = new ArrayList<>();
//角色必须以ROLE_开头
for (Role role : user.getRoles()) {
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" role.getNameEN());
list.add(authority);
}
//创建User对象
userdetail = new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), list);
}
return userdetail;
}
}
这里需要注意要把信息数据放在org.springframework.security.core.userdetails.User userdetail = null; 的User实体类里面然后返回。 接下来就可以测试效果了。
测试效果
数据库信息如下
一个需要登录的demo
根据数据库的信息登录之后
可以注销之后登录lisi
Demo源码
链接:https://pan.baidu.com/s/1XwuSRA5cxIf22zXs0KRb9A 提取码:bjz4