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)示例代码如下
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)
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class MySecurityConfig extends WebSecurityConfigurerAdapter { }
加上@EnableGlobalMethodSecurity(prePostEnabled = true)注解之后在方法层面,就可以手动使用使用@PreAuthorize 指定在方法之前进行角色的认证,指定方法可以访问的角色列表。
如:hasAnyRole('角色名称 1','角色名称 N');
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、或者在配置类中,进行权限的统一配置
如以下编写的配置类:
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、 数据库脚本
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。
配置数据库
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
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
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都来源于数据库。
因此配置文件的配置也是如此。
@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
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