Spring学习笔记(三十四)——Springboot集成Spring Security

2022-09-26 18:16:22 浏览数 (1)

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

  1. 创建 maven 项目
  2. 加入依赖:spring boot 依赖, spring security 依赖
代码语言:javascript复制
<!--加入 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>
  1. 创建 Controller,接收请求
代码语言:javascript复制
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 安全管理框架";
    }
}
  1. 框架生成的用户 用户名: user 密码: 在启动项目时,生成的临时密码。uuid 日志中生成的密码 generated security password: 9717464c-fafd-47b3-9995-2c18b24f7336
  2. 自定义用户名和密码 需要在 springboot 配置文件中设置登录的用户名和密码 在 resource 目录下面创建 spring boot 配置文件 application.yml(application.properties) security: user: name: Nick password: Nick name:自定义用户名称 password:自定义密码
  1. 关闭验证 //排除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" 实现密码加密:

  • 创建用来加密的实现类(选择一种加密的算法)
代码语言:javascript复制
@Bean
public PasswordEncoder passwordEncoder(){
    //创建 PasawordEncoder 的实现类, 实现类是加密算法
    return new BCryptPasswordEncoder();
}
  • 给每个密码加密
代码语言:javascript复制
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);
}
  1. 编写实现了UserDetailsService接口的Sercie层 UserService.java
代码语言:javascript复制
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

0 人点赞