Skip to content

[TOC]

一、Spring Security简介

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。Spring Security致力于为Java应用程序提供身份验证和授权的能力。像所有Spring项目一样,Spring Security的真正强大之处在于它可以轻松扩展以满足定制需求的能力。

角色和权限时许

Spring Security两大重要核心功能:用户认证(Authentication)用户授权(Authorization)

  • 用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
  • 用户授权:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,有的用户既能读取,又能修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。 RBAC

二、快速开始

使用Springboot工程搭建Spring Security项目。

1.引入依赖

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 https://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.4.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.qf</groupId>
    <artifactId>spring-security-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-security-demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </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.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

在pom中新增了Spring Security的依赖

xml
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2.创建测试访问接口

用于访问接口时触发Spring Security登陆页面

java
@RestController
public class SecurityController {

    @RequestMapping("/add")
    public String add(){
        return "hello security!";
    }

}

3.访问接口,自动跳转至Security登陆页面

访问add接口,讲自动跳转至Security的登陆页面

image-20210305105043157

默认账号是: user

默认密码是:启动项目的控制台中输出的密码

快速开始

三、原理剖析

在上一节中访问add接口,发现被Spring Security的登陆页面拦截,可以猜到这是触发了Security框架的过滤器。Spring Security本质上就是一个过滤器链。下面讲介绍Security框架的过滤器链。

1.过滤器链

  • FilterSecurityInterceptor:是一个方法级的权限过滤器,位于过滤器链的最底部。
  • ExceptionTranslationFilter: 异常过滤器,用来处理在认证授权过程中抛出异常。
  • UsernamePasswordAuthenticationFilter: 用于对/login的POST请求做拦截,校验表单中的用户名和密码。

2.过滤器加载过程

Springboot在整合Spring Security项目时会自动配置DelegatingFilterProxy过滤器,若非Springboot工程,则需要手动配置该过滤器。

springsecurity过滤器链

image-20210504180253654

过滤器如何进行加载的? 可以通过断点 查看 bean 这个bean 是 FilterChainProxy

结合上图和源码,Security在DelegatingFilterProxy的doFilter()调用了initDelegat()方法,在该方法中调用了WebApplicationContext的getBean()方法,该方法触发FilterChainProxy的doFilterInternal方法,用于获取过滤链中的所有过滤器并进行加载。

image-20210504180355381

3.Security的两个关键接口

在快速开始中发现Spring Security使用了默认的用户名和密码,实际用户名和密码需要自定义,因此会用到以下两个接口。下述两个接口的具体实现将在之后的例子中体现。

1) UserDetailsService接口

若需要从数据库中获取用户名和密码,则需要把查询数据库的过程写在这个接口里。

2)PasswordEncoder接口

在密码的处理上,需要进行编解码器,该接口实现对密码进行加密。

四、多种方式配置登陆的用户名和密码

1.通过配置文件设置用户名和密码

yaml
# 方式一:设置登陆的用户名和密码
spring:
  security:
    user:
      name: qfadmin
      password: 123456

2.通过创建配置类实现设置

java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //用于密码的密文处理
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        //生成密文
        String password = passwordEncoder.encode("123456");
        //设置用户名和密码
        auth.inMemoryAuthentication().withUser("qfAdmin").password(password).roles("admin");
    }
  
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

3.编写自定义实现类(常用)

第一步:编写UserDetailsService实现类,可以从数据库中获取用户名和密码

java
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
    
    //接收传过来的用户名   根据传过来的用户名查询数据库的密码  返回 数据库中的  用户名 密码  权限信息
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //设置角色,角色的概念在之后章节介绍
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        //可以从数据库获取用户名和密码   这里返回的是数据库中的数据  返回意味着认证成功 
        return new User("qfAdmin",new BCryptPasswordEncoder().encode("123456"),auths);
    }
}

第二步:编写配置类

java
@Configuration
public class SecurityConfigByImpl extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login.html") //设置自定义登陆页面 注意如果不通过controller 跳转这个页面需要放在static中
                .loginProcessingUrl("/usr/login") //登陆时访问的路径 也就s  这个路径表示表单提交  处理登录请求的controller 不需要我们写  security 帮我们做到了  
                .defaultSuccessUrl("/index").permitAll() //登陆成功后跳转的路径    注意 是你没有指定跳转目标时 才跳转到这个路径   必须加这个配置  要不然  页面无法正确重定向
                .and().authorizeRequests()
                    .antMatchers("/","/add","/user/login").permitAll() //设置可以直接访问的路径,取消拦截
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

为了测试顺利,这里临时关闭csrf防护。所谓csrf防护,全称为跨站请求伪造(Cross-site request forgery),是一种网络攻击方式,CSRF攻击利用网站对于用户网页浏览器的信任,挟持用户当前已登陆的Web应用程序,去执行并非用户本意的操作。简而言之,用户通过盗取目标网站保存的cookie中的用户信息,实现非法使用。

其中,login.html为自己提供的登陆页面,具体内容如下:

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/usr/login" method="post">
        用户名:<input type="text" name="username"/><br/>
        密码:<input type="password" name="password"><br/>
        <input type="submit" value="login"/>
    </form>

</body>
</html>

注意:表单提交的地址为配置类中配置的登陆时访问路径:/usr/login

第三步:在controller中添加/index接口

注意 添加 thymeleaf 依赖

java
@RestController
public class SecurityController {

    @RequestMapping("/add")
    public String add(){
        return "hello security!";
    }

    @RequestMapping("/index")
    public String index(){
        return "hello index";
    }

}

五、基于角色和权限进行访问控制

Spring Security提供了四个方法用于角色和权限的访问控制。通过这些方法,对用户是否具有某个或某些权限,进行过滤访问。对用户是否具备某个或某些角色,进行过滤访问。

1.hasAuthority方法

判断当前主体(用户)是否有指定的权限,有返回true,否则返回false

该方法适用于只拥有一个权限的用户。

1)在配置类中设置当前主体具有怎样的权限才能访问。

java
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //配置没有权限的跳转页面
        http.exceptionHandling().accessDeniedPage("/error.html");
        http.formLogin()
                .loginPage("/login.html") //设置自定义登陆页面
                .loginProcessingUrl("/usr/login") //登陆时访问的路径
                .defaultSuccessUrl("/index").permitAll() //登陆成功后跳转的路径
                .and().authorizeRequests()
                    .antMatchers("/","/add","/user/login").permitAll() //设置可以直接访问的路径,取消拦截
                    //1.hasAuthority方法:当前登陆用户,只有具有admin权限才可以访问这个路径
                    .antMatchers("/index").hasAuthority("admin")
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

2)在userdetailsService,为返回的User对象设置权限

java
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //因目前还没引入角色的概念,先用工具类快速生成角色
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
        //可以从数据库获取用户名和密码
        return new User("qfAdmin",new BCryptPasswordEncoder().encode("123456"),auths);
    }

2.hasAnyAuthority方法

适用于一个主体有多个权限的情况,多个权限用逗号隔开。

java
 @Override
    protected void configure(HttpSecurity http) throws Exception {
        //注销的配置
        http.logout().logoutUrl("/logout") //注销时访问的路径
                .logoutSuccessUrl("/logoutSuccess").permitAll(); //注销成功后访问的路径

        //配置没有权限的跳转页面
        http.exceptionHandling().accessDeniedPage("/error.html");
        http.formLogin()
                .loginPage("/login.html") //设置自定义登陆页面
                .loginProcessingUrl("/usr/login") //登陆时访问的路径
//                .defaultSuccessUrl("/index").permitAll() //登陆成功后跳转的路径
                .defaultSuccessUrl("/success.html").permitAll() //登陆成功后跳转的路径
                .and().authorizeRequests()
                    .antMatchers("/","/add","/user/login").permitAll() //设置可以直接访问的路径,取消拦截
                    //2.hasAnyAuthority方法:当前登陆用户,具有admin或manager权限可以访问这个路径
                    .antMatchers("/index").hasAnyAuthority("admin,manager")
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

3.hasRole方法

如果用户具备给定角色就允许访问,否则报403错误。

1)修改配置类

java
 @Override
    protected void configure(HttpSecurity http) throws Exception {
        //注销的配置
        http.logout().logoutUrl("/logout") //注销时访问的路径
                .logoutSuccessUrl("/logoutSuccess").permitAll(); //注销成功后访问的路径

        //配置没有权限的跳转页面
        http.exceptionHandling().accessDeniedPage("/error.html");
        http.formLogin()
                .loginPage("/login.html") //设置自定义登陆页面
                .loginProcessingUrl("/usr/login") //登陆时访问的路径
//                .defaultSuccessUrl("/index").permitAll() //登陆成功后跳转的路径
                .defaultSuccessUrl("/success.html").permitAll() //登陆成功后跳转的路径
                .and().authorizeRequests()
                    .antMatchers("/","/add","/user/login").permitAll() //设置可以直接访问的路径,取消拦截
                    //3.hasRole方法:当前主体具有指定角色,则允许访问
                    .antMatchers("/index").hasRole("student")
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

2)修改user对象

java
//权限设置
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //因目前还没引入角色的概念,先用工具类快速生成角色
        //hasRole:  由于源码会把role加上"ROLE_",因此在这里设计角色时需加上前缀
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_student");
        //可以从数据库获取用户名和密码
        return new User("qfAdmin",new BCryptPasswordEncoder().encode("123456"),auths);
    }

其中角色student需要在设置时加上“ROLE_”前缀,因为通过源码hasRole方法给自定义的角色名前加上了“ROLE_”前缀

java
private static String hasRole(String role) {
        Assert.notNull(role, "role cannot be null");
        Assert.isTrue(!role.startsWith("ROLE_"), () -> {
            return "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'";
        });
        return "hasRole('ROLE_" + role + "')";
    }

4.hasAnyRole方法

设置多个角色,多个角色之间使用逗号隔开,只要用户具有某一个角色,就能访问。

java
 @Override
    protected void configure(HttpSecurity http) throws Exception {
        //注销的配置
        http.logout().logoutUrl("/logout") //注销时访问的路径
                .logoutSuccessUrl("/logoutSuccess").permitAll(); //注销成功后访问的路径

        //配置没有权限的跳转页面
        http.exceptionHandling().accessDeniedPage("/error.html");
        http.formLogin()
                .loginPage("/login.html") //设置自定义登陆页面
                .loginProcessingUrl("/usr/login") //登陆时访问的路径
//                .defaultSuccessUrl("/index").permitAll() //登陆成功后跳转的路径
                .defaultSuccessUrl("/success.html").permitAll() //登陆成功后跳转的路径
                .and().authorizeRequests()
                    .antMatchers("/","/add","/user/login").permitAll() //设置可以直接访问的路径,取消拦截
                    //4.hasAnyRole方法:当前主体只要具备其中某一个角色就能访问
                    .antMatchers("/index").hasAnyRole("student1,teacher")
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

六、SpringSecurity的常用注解

1、@Secured注解

@Secured注解用于校验用户具有某个角色,才可以访问方法

1)启动类上开启注解

java
@EnableGlobalMethodSecurity(securedEnabled = true)

2)在方法上配置注解

java
		/**
     * 测试@Secured注解
     * @return
     */
    @RequestMapping("/items")
    @Secured({"ROLE_student"})
    public String items(){
        return "show items";
    }

3)用户对象中设置角色

java
		@Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_student");
        //可以从数据库获取用户名和密码
        return new User("qfAdmin",new BCryptPasswordEncoder().encode("123456"),auths);
    }

2、@PreAuthorize

进入方法前的权限验证

步骤

  • 在启动类上开启注解
java
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
  • 在方法上使用注解
java
 @RequestMapping("/items")
 @PreAuthorize("hasAnyAuthority('admin')")
 public String items(){
     return "show itemds";
 }

注意:方法参数是之前介绍的四个方法。

3、@PostAuthorize

在方法访问之后进行校验,实际使用并不多

步骤

  • 启动类上开启注解
java
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
  • 方法上使用注解
java
@RequestMapping("/postItems")
    @PostAuthorize("hasAnyAuthority('teacher')")
    public String postItems(){
        //先执行方法内容,再做权限校验
        System.out.println("show detail here...");
        return "show post items";
    }

4、@PostFilter

权限验证之后对数据进行过滤,只能获取满足条件的数据

步骤

  • 在方法上使用注解
java
@RequestMapping("/postFilterItems")
    @PreAuthorize("hasAnyAuthority('admin')")
    @PostFilter("filterObject.userName == 'xiaoming'")
    public List<User> getUsers(){

        ArrayList<User> list = new ArrayList<User>();
        list.add(new User(1L,"xiaowang"));
        list.add(new User(2L,"xiaoming"));
        return list;
    }
  • 访问接口,发现list集合中中获取了满足条件的xiaoming对象

5、@PreFilter

对传入方法的数据进行过滤

步骤

  • 在方法上使用注解
java
@RequestMapping("/preFilterItems")
    @PreAuthorize("hasAnyAuthority('admin')")
    @PreFilter(value="filterObject.userName == 'xiaoming'")
    public List<User> getUsersByPreFilter(@RequestBody List<User> list){
        //只有userName是'xiaoming'的数据才会被传入
        list.forEach(t->{
            System.out.println(t.getUserName());
        });
        return list;
    }
  • 访问方法,发现只有userName是'xiaoming'的数据才会被传入

七、用户注销

1.在配置类添加注销的配置

java
 @Override
    protected void configure(HttpSecurity http) throws Exception {
        //注销的配置
        http.logout().logoutUrl("/logout") //注销时访问的路径
                .logoutSuccessUrl("/logoutSuccess").permitAll(); //注销成功后访问的路径

        //配置没有权限的跳转页面
        http.exceptionHandling().accessDeniedPage("/error.html");
        http.formLogin()
                .loginPage("/login.html") //设置自定义登陆页面
                .loginProcessingUrl("/usr/login") //登陆时访问的路径
//                .defaultSuccessUrl("/index").permitAll() //登陆成功后跳转的路径
                .defaultSuccessUrl("/success.html").permitAll() //登陆成功后跳转的路径
                .and().authorizeRequests()
                    .antMatchers("/","/add","/user/login").permitAll() //设置可以直接访问的路径,取消拦截
                    //1.hasAuthority方法:当前登陆用户,只有具有admin权限才可以访问这个路径
                    //.antMatchers("/index").hasAuthority("admin")
                    //2.hasAnyAuthority方法:当前登陆用户,具有admin或manager权限可以访问这个路径
                    //.antMatchers("/index").hasAnyAuthority("admin,manager")
                    //3.hasRole方法:当前主体具有指定角色,则允许访问
                    //.antMatchers("/index").hasRole("student")
                    //4.hasAnyRole方法:当前主体只要具备其中某一个角色就能访问
                    .antMatchers("/index").hasAnyRole("student1,teacher")
                .anyRequest().authenticated()
                .and().csrf().disable(); //关闭csrf防护
    }

2.设置注销链接

添加success.html页面作为登陆成功后的跳转页面

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    登陆成功 <a href="/logout">退出</a>
</body>
</html>

登陆后访问退出按钮,实现注销功能。

八、个人补充

8.1 和shiro做比较

spring security 特点

  • 与spring无缝整合

  • 全面的权限控制

  • 专门为Web开发设计

    • ​ 旧版本不能脱离web环境使用
    • 新版本对整个框架进行了分层抽取,分成了核心模块和web模块,单独引入核心模块就可以脱离web环境
  • 重量级

shiro的特点

  • apache旗下轻量级权限控制框架
  • 轻量级 shiro主张的理念把复杂的事情变简单,针对性能有更高要求的互联网应用有更好的表现
  • 通用性
    • 好处 不局限于web环境,可以脱离web使用
    • 缺陷 在web环境下的 一些特定的需求需要手动编写代码定制

spring security 是spring家族的一个安全框架 在springboot出现之前 spring security 就已经发展多年了,但是使用的并不多,安全管理这个领域 一直是shiro 的天下。相对于shiro ,在ssm整合spring security都是比较麻烦的操作,所以 spring security 虽然功能比shiro 强大,但是反而没有shiro 使用的多(shiro虽然没有security强大,但是对于大部分项目而言也已经够用了) 但是有了spring boot之后 spring boot对security 提供了自动化配置方案 可以使用较少的配置来使用 spring security

因此 一般来说 常见的安全管理技术栈的组合是这样的 注意 是一般来讲 ,实际上无论怎么组合 都是可以的

  • ssm + shiro
  • spring boot /cloud + security

8.2 入门案例

创建springboot工程

添加依赖 主要是 security 和 web

xml
	<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
	<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>

	<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

image-20210504164529424

创建一个 controller

java
package com.glls.springsecuritydemo1.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class SecurityController {

    @GetMapping("/index")
    public String index(){
        return "success";
    }
}

运行启动类 看到启动信息 看到等下要使用的密码

image-20210504165113596

访问 那个 controller 跳转到一个认证页面

image-20210504165211693

默认的用户名:user 密码在项目启动的时候在控制台会打印,

注意每次启动的时候密码都回发生变化!

image-20210504165257089

认证成功 跳转到刚才想要访问的controller

image-20210504165359368

8.3 基本原理

spring security 本质是一个过滤器链

java
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil
ter
org.springframework.security.web.context.SecurityContextPersistenceFilter 
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter 
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter 
org.springframework.security.web.session.SessionManagementFilter 
org.springframework.security.web.access.ExceptionTranslationFilter 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor

重点了解三个过滤器

FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。

image-20210504171305003

super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。

fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务。

ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常

image-20210504172650984

UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户 名,密码。

image-20210504173133152

理解: 一系列的过滤器 对请求进行 处理 比如 认证 授权 异常处理

这些过滤器是如何加载的? 参考上文

8.4 使用security 有两个重要接口

UserDetailsService 接口

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中 账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。

如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下

image-20210504180934950

返回值 UserDetails

这个类是系统默认的用户“主体”

java

package org.springframework.security.core.userdetails;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import java.io.Serializable;
import java.util.Collection;

/**
 * Provides core user information.
 *
 * <p>
 * Implementations are not used directly by Spring Security for security purposes. They
 * simply store user information which is later encapsulated into {@link Authentication}
 * objects. This allows non-security related user information (such as email addresses,
 * telephone numbers etc) to be stored in a convenient location.
 * <p>
 * Concrete implementations must take particular care to ensure the non-null contract
 * detailed for each method is enforced. See
 * {@link org.springframework.security.core.userdetails.User} for a reference
 * implementation (which you might like to extend or use in your code).
 *
 * @see UserDetailsService
 * @see UserCache
 *
 * @author Ben Alex
 */
public interface UserDetails extends Serializable {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Returns the authorities granted to the user. Cannot return <code>null</code>.
	 *
	 * @return the authorities, sorted by natural key (never <code>null</code>)
	   // 表示获取登录用户所有权限

	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * Returns the password used to authenticate the user.
	 *  
	 * @return the password
	 	获取密码
	 */
	String getPassword();

	/**
	 * Returns the username used to authenticate the user. Cannot return <code>null</code>.
	 *
	 * @return the username (never <code>null</code>)
	 获取用户名
	 */
	String getUsername();

	/**
	 * Indicates whether the user's account has expired. An expired account cannot be
	 * authenticated.
	 *
	 * @return <code>true</code> if the user's account is valid (ie non-expired),
	 * <code>false</code> if no longer valid (ie expired)
	 判断账户是否过期
	 */
	boolean isAccountNonExpired();

	/**
	 * Indicates whether the user is locked or unlocked. A locked user cannot be
	 * authenticated.
	 *
	 * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
	 判断账户是否被锁定
	 */
	boolean isAccountNonLocked();

	/**
	 * Indicates whether the user's credentials (password) has expired. Expired
	 * credentials prevent authentication.
	 *
	 * @return <code>true</code> if the user's credentials are valid (ie non-expired),
	 * <code>false</code> if no longer valid (ie expired)
	 
	 判断凭证(密码)是否过期
	 */
	boolean isCredentialsNonExpired();

	/**
	 * Indicates whether the user is enabled or disabled. A disabled user cannot be
	 * authenticated.
	 *
	 * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
	 当前用户是否可用
	 */
	boolean isEnabled();
}

image-20210504181644002

咱们只需关注其实现类 User 即可

image-20210504181742798

方法参数 username 表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无 法接收。

PasswordEncoder 接口

java
/*
 * Copyright 2011-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.security.crypto.password;

/**
 * Service interface for encoding passwords.
 *
 * The preferred implementation is {@code BCryptPasswordEncoder}.
 *
 * @author Keith Donald
 */
public interface PasswordEncoder {

	/**
	 * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
	 * greater hash combined with an 8-byte or greater randomly generated salt.
	 把参数按照特定的解析规则进行解析
	 */
	String encode(CharSequence rawPassword);

	/**
	 * Verify the encoded password obtained from storage matches the submitted raw
	 * password after it too is encoded. Returns true if the passwords match, false if
	 * they do not. The stored password itself is never decoded.
	 *
	 * @param rawPassword the raw password to encode and match
	 * @param encodedPassword the encoded password from storage to compare with
	 * @return true if the raw password, after encoding, matches the encoded password from
	 * storage
	 
	  验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹
      配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个
      参数表示存储的密码。

	 */
	boolean matches(CharSequence rawPassword, String encodedPassword);

	/**
	 * Returns true if the encoded password should be encoded again for better security,
	 * else false. The default implementation always returns false.
	 * @param encodedPassword the encoded password to check
	 * @return true if the encoded password should be encoded again for better security,
	 * else false.
	 如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回
	 false。默认返回 false
	 */
	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}
}

image-20210504182422676

BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析 器。 BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单 向加密。可以通过 strength 控制加密强度,默认 10.

  • 实例
java
package com.glls.springsecuritydemo1;

import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class TestEncoder {
    @Test
    public void test01(){
// 创建密码解析器
        BCryptPasswordEncoder bCryptPasswordEncoder = new
                BCryptPasswordEncoder();
// 对密码进行加密
        String name = bCryptPasswordEncoder.encode("glls");
// 打印加密之后的数据
        System.out.println("加密之后数据:\t"+name);
//判断原字符加密后和加密之前是否匹配
        boolean result = bCryptPasswordEncoder.matches("glls", name);
// 打印比较结果
        System.out.println("比较结果:\t"+result);
    }
}

8.5 SpringSecurity Web 权限方案

设置登录系统的账号、密码

方式一:在 application.properties

xml
spring:
  security:
    user:
      name: glls
      password: 123456

方式二:编写类实现接口 SecurityConfig 和 UserDetailsService

java
package com.glls.springsecuritydemo1.config;

import org.springframework.beans.factory.annotation.Autowired;
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.builders.HttpSecurity;
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
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // 表单登录
                .and()
                .authorizeRequests() // 认证配置
                .anyRequest() // 任何请求
                .authenticated(); // 都需要身份验证
    }


    // 注入 PasswordEncoder 类到 spring 容器中
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


}
java
package com.glls.springsecuritydemo1.config;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @ClassName : LoginService
 * @Author : glls
 * @Date: 2021/5/4 20:13
 * @Description :
 */

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

     

        // 判断用户名是否存在
        if (!"admin".equals(username)) {
            throw new UsernameNotFoundException("用户名不存在!");
        }


        // 从数据库中获取的密码 123456 的密文
        String pwd =
                "$2a$10$mXydam1p7dcGGWWTfdUH..feST5wWPlMnXMV1t/7nZjTCWsSJrF1y";
// 第三个参数表示权限
        return new User("glls",pwd,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin,"));
    }

}

实现数据库认证来完成用户登录

image-20211124144138221

RBAC 基于角色的权限控制

完成自定义登录

  • 准备sql

    sql
    create table users(
     id bigint primary key auto_increment,
    username varchar(20) unique not null,
    password varchar(100)
    );
    -- 密码 123456
    insert into users values(1,'zs','$2a$10$mXydam1p7dcGGWWTfdUH..feST5wWPlMnXMV1t/7nZjTCWsSJrF1y');
    -- 密码 1234567
    insert into users values(2,'ls','$2a$10$fJTxLZ9G6I/J0WCTC7B0tuTbsqxQKJPDpdYORXXGSgMDpcN334mfG');
    
    
    create table role(
    id bigint primary key auto_increment,
    name varchar(20)
    );
    
    insert into role values(1,'管理员');
    insert into role values(2,'普通用户');
    
    
    create table role_user(
    uid bigint,
    rid bigint
    );
    insert into role_user values(1,1);
    insert into role_user values(2,2);
    
    create table menu(
    id bigint primary key auto_increment,
    name varchar(20),
    url varchar(100),
    parentid bigint,
    permission varchar(20)
    );
    
    
    insert into menu values(1,'系统管理','',0,'menu:system');
    insert into menu values(2,'用户管理','',0,'menu:user');
    
    create table role_menu(
    mid bigint,
    rid bigint
    );
    insert into role_menu values(1,1);
    
    insert into role_menu values(2,1);
    insert into role_menu values(2,2);
  • 添加依赖

    shell
    <dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-security</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-devtools</artifactId>
    			<scope>runtime</scope>
    			<optional>true</optional>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-configuration-processor</artifactId>
    			<optional>true</optional>
    		</dependency>
    		<dependency>
    			<groupId>org.projectlombok</groupId>
    			<artifactId>lombok</artifactId>
    			<optional>true</optional>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    			<exclusions>
    				<exclusion>
    					<groupId>org.junit.vintage</groupId>
    					<artifactId>junit-vintage-engine</artifactId>
    				</exclusion>
    			</exclusions>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.security</groupId>
    			<artifactId>spring-security-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    		
    		<!--mybatis-plus-->
     <dependency>
     <groupId>com.baomidou</groupId>
     <artifactId>mybatis-plus-boot-starter</artifactId>
     <version>3.0.5</version>
     </dependency>
     <!--mysql-->
     <dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     </dependency>
  • 实体类

    java
    @Data
    public class Users {
    
        private Integer id;
        private String username;
        private String password;
    
    }
  • mybatisplus配置

    java
    package com.glls.springsecuritydemo2.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.glls.springsecuritydemo2.pojo.Users;
    import org.apache.ibatis.annotations.Mapper;
    import org.springframework.stereotype.Repository;
    
    
    @Repository     // 这个注解的作用是注入的时候 不报红色警告
    public interface UsersMapper extends BaseMapper<Users> {
    }

    启动类扫描mapper

    java
    @MapperScan(basePackages = {"com.glls.springsecuritydemo2.mapper"})

    配置文件

    yml
    spring:
      datasource:
        username: root
        password: 123456
        url: jdbc:mysql://127.0.0.1:3306/springsecurity?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
        driver-class-name: com.mysql.cj.jdbc.Driver
    
    logging:
      level:
        com.glls: debug
  • UserDetailsService 接口 注入自定义逻辑

java
package com.glls.springsecuritydemo2.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.glls.springsecuritydemo2.mapper.UsersMapper;
import com.glls.springsecuritydemo2.pojo.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;


@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private UsersMapper usersMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<Users> wrapper = new QueryWrapper();
        wrapper.eq("username",username);
        Users users = usersMapper.selectOne(wrapper);
        if(users == null) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        System.out.println(users);
        List<GrantedAuthority> auths =
                AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new User(users.getUsername(),users.getPassword(),auths);

    }
}
  • 测试登录

  • 未认证请求跳转到登录页 自定义登录页

    • 引入前端模板依赖

      shell
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
      </dependency>
    • 配置 关闭缓存

      yml
      spring:
        datasource:
          username: root
          password: 123456
          url: jdbc:mysql://127.0.0.1:3306/springsecurity?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
          driver-class-name: com.mysql.cj.jdbc.Driver
        thymeleaf:
          cache: false #关闭缓存
      
      
      logging:
        level:
          com.glls: debug
    • 引入登录页面

html
<!DOCTYPE html>
<!-- 需要添加
<html  xmlns:th="http://www.thymeleaf.org">
这样在后面的th标签就不会报错
 -->
<html  xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>表单提交</h1>
<!-- 注意  action 需要和 下面配置类指定的路径一致 method 和 name 属性值 由过滤器决定了所以 默认为  下面的固定值  -->
   <!-- UsernamePasswordAuthenticationFilter 这个过滤器决定了请求方式 参数名称 当然 可以自定义参数名称  -->
<form action="/login"  method="post">
   
    <input type="text" name="username" /> <br>
    <input type="password" name="password" /> <br>
    <input type="submit" value="提交"/>
</form>
</body>
</html>
  • 编写controller

    java
    @Controller
    public class IndexController {
    
            @GetMapping("/index")
            public String index(){
                return "login";
            }
        
        @PostMapping("/main")
        public String success(){
            return "main";
        }
    
    
    }
    java
    @RestController
    @RequestMapping("/user")
    public class UsersController {
    
        @GetMapping("/findAll")
        public String findAll(){
            return "findAll";
        }
    
        @GetMapping("/anno")
        public String anno(){
            return "不需要认证可以访问";
        }
    }
  • 编写配置类放行登录页面以及静态资源

java
package com.glls.springsecuritydemo2.config;

import org.springframework.beans.factory.annotation.Autowired;
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.builders.HttpSecurity;
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
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.formLogin()  // 自定义登录页面
                .loginPage("/login.html")  // 指定登录页面路径  如果直接是页面  必须在static中
                //.loginPage("/index")  // 指定登录页面路径  通过controller 跳转  则在templates 中
                .loginProcessingUrl("/login")
               
                .defaultSuccessUrl("/main").permitAll() // 登陆成功之后跳转路径
               
                .and()
                .authorizeRequests()
                .antMatchers("/layui/**","/user/anno") //配置无需认证的请求路径
                .permitAll() // 指定 URL 无需保护。
                .anyRequest() // 其他请求
                .authenticated() //需要认证
                .and().csrf().disable(); //关闭csrf防护  这个一定要加上
    }




    // 注入 PasswordEncoder 类到 spring 容器中
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

测试 无需认证的访问路径

image-20210505140918235

测试 需要认证的访问路径 直接跳转到登录页面

image-20210505141153890

登录 成功 跳转到 之前的访问路径

image-20210505141321580

  • 自定义表单参数

    html
    <form action="/login"method="post">
    用户名:<input type="text"name="myname"/><br/>
    密码:<input type="password"name="mypassword"/><br/>
    <input type="submit"value="提交"/>
    </form>
    java
    // 配置类也需要改
      @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http.formLogin()  // 自定义登录页面
                    .loginPage("/login.html")  // 指定登录页面路径  如果直接是页面  必须在static中
                    //.loginPage("/index")  // 指定登录页面路径  通过controller 跳转  则在templates 中
                    .loginProcessingUrl("/login")
                    .defaultSuccessUrl("/main").permitAll() // 登陆成功之后 默认 跳转路径
                    .usernameParameter("myusername")
                    .passwordParameter("mypassword")
                    .and()
                    .authorizeRequests()
                    .antMatchers("/layui/**","/user/anno") //表示配置请求路径
                    .permitAll() // 指定 URL 无需保护。
                    .anyRequest() // 其他请求
                    .authenticated() //需要认证
                    .and().csrf().disable(); //关闭csrf防护  这个一定要加上
        }

8.6 基于角色或权限进行访问控制

参考上文 五

8.6.1 hasAuthority 方法

​ 如果当前的主体具有指定的权限,则返回 true,否则返回 false

8.6.2 hasAnyAuthority 方法

​ 如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回 true.

8.6.3 hasRole 方

​ 如果用户具备给定角色就允许访问,否则出现 403。 如果当前主体具有指定的角色,则返回 true。

8.6.4 hasAnyRole

​ 表示用户具备任何一个角色都可以访问。

8.7 基于数据库实现权限认证

8.7.1 实体类

java
@Data
@Alias("menu")
public class Menu {
    private Long id;
    private String name;
    private String url;
    private Long parentId;
    private String permission;
}
java
@Data
@Alias("role")
public class Role {
    private Long id;
    private String name;
}

8.7.2 mybatis-plus配置

接口定义查询 角色 和 权限的方法

java
 /**
     * 根据用户 Id 查询用户角色
     * @param userId
     * @return
     */
    List<Role> selectRoleByUserId(Integer userId);
    /**
     * 根据用户 Id 查询菜单
     * @param userId
     * @return
     */
    List<Menu> selectMenuByUserId(Integer userId);

映射文件

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.glls.springsecuritydemo3.mapper.UsersMapper">

    <!--根据用户 Id 查询角色信息-->
    <select id="selectRoleByUserId" resultType="role">
        SELECT r.id,r.name FROM role r INNER JOIN role_user ru ON
        ru.rid=r.id where ru.uid=#{0}
    </select>
    <!--根据用户 Id 查询权限信息-->
    <select id="selectMenuByUserId" resultType="menu">
        SELECT m.id,m.name,m.url,m.parentid,m.permission FROM menu m
        INNER JOIN role_menu rm ON m.id=rm.mid
        INNER JOIN role r ON r.id=rm.rid
        INNER JOIN role_user ru ON r.id=ru.rid
        WHERE ru.uid=#{0}
    </select>
</mapper>

配置文件扫描映射文件

yml
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml

修改MyUserDetailsService 的 loadUserByUsername 方法

java
 @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<Users> wrapper = new QueryWrapper();
        wrapper.eq("username",username);
        Users users = usersMapper.selectOne(wrapper);
        if(users == null) {
            throw new UsernameNotFoundException("用户名不存在!");
        }

        //查询 权限 和 角色  然后 封装到 User 中
        List<GrantedAuthority> auths = new ArrayList<>();
        //查询用户角色列表
        List<Role> roles = usersMapper.selectRoleByUserId(users.getId());
        //查询用户权限列表
        List<Menu> menus = usersMapper.selectMenuByUserId(users.getId());

        //处理角色  拼接   ROLE_xxx
        for(Role role: roles){
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_"+role.getName());
            auths.add(simpleGrantedAuthority);
        }
        //处理权限
        for(Menu menu:menus){
            auths.add(new SimpleGrantedAuthority(menu.getPermission()));
        }


        // 注意  new User(String username, String password, Collection<? extends GrantedAuthority> authorities)
        // 这第二个参数 是加密之后的 密码
        return new User(users.getUsername(), users.getPassword(),auths);

    }

修改 访问配置类 进行测试 使用 管理员 和 非管理员测试

java
@Override
    protected void configure(HttpSecurity http) throws Exception {

        http.formLogin()  // 自定义登录页面
                .loginPage("/login.html")  // 指定登录页面路径  如果直接是页面  必须在static中
                //.loginPage("/index")  // 指定登录页面路径  通过controller 跳转  则在templates 中
                .loginProcessingUrl("/login")
                //.successForwardUrl("/main")  // 登录页面表单的请求路径
                .defaultSuccessUrl("/main").permitAll() // 登陆成功之后 默认 跳转路径
                //.failureForwardUrl("/fail")    // 登录失败跳转到哪个url
                .usernameParameter("myusername")
                .passwordParameter("mypassword")
                .and()
                .authorizeRequests()
                .antMatchers("/layui/**","/user/anno") //表示配置请求路径
                .permitAll() // 指定 URL 无需保护。
                //1.hasAuthority方法:当前登陆用户,只有具有admin权限才可以访问这个路径
                //.antMatchers("/user/findAll").hasAuthority("menu:system")
                //2.hasAnyAuthority方法:当前登陆用户,具有admin或manager权限可以访问这个路径
                .antMatchers("/user/findAll").hasAnyAuthority("menu:user")
                //3.hasRole方法:当前主体具有指定角色,则允许访问
                //.antMatchers("/user/findAll").hasRole("管理员")
                //4.hasAnyRole方法:当前主体只要具备其中某一个角色就能访问
                //.antMatchers("/user/findAll").hasAnyRole("管理员,普通用户")
                .anyRequest() // 其他请求
                .authenticated() //需要认证
                .and().csrf().disable(); //关闭csrf防护  这个一定要加上
    }

8.8 自定义 403 页面

修改访问配置类

image-20210505160301650

java
http.exceptionHandling().accessDeniedPage("/unauth");

添加controller 方法

java
    @GetMapping("/unauth")
    public String accessDenyPage(){
        return "unauth";
    }

image-20210505160352843

image-20210505160310902

8.9 注解使用

@Secured

判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。 使用注解先要开启注解功能! @EnableGlobalMethodSecurity(securedEnabled=true)

java
@SpringBootApplication
@MapperScan(basePackages = {"com.glls.springsecuritydemo3.mapper"})
@EnableGlobalMethodSecurity(securedEnabled = true)
public class Springsecuritydemo3Application {

	public static void main(String[] args) {
		SpringApplication.run(Springsecuritydemo3Application.class, args);
	}

}

在控制器方法上添加注解

java
    // 测试注解  
    @RequestMapping("/testSecured")
    @ResponseBody
    @Secured({"ROLE_普通用户1","ROLE_管理员"})
    public String helloUser() {
        return "hello,user";
    }

使用不同的角色测试

html
http://localhost:8080/testSecured

@PreAuthorize

先开启注解功能: @EnableGlobalMethodSecurity(prePostEnabled = true)

@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用 户的 roles/permissions 参数传到方法中。

java
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
java
    @RequestMapping("/preAuthorize")
    @ResponseBody
//@PreAuthorize("hasRole('ROLE_管理员')")
    @PreAuthorize("hasAnyAuthority('menu:system')")
    public String preAuthorize(){
        System.out.println("preAuthorize");
        return "preAuthorize";
    }

@PostAuthorize

先开启注解功能: @EnableGlobalMethodSecurity(prePostEnabled = true) @PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值 的权限.

java
    @RequestMapping("/testPostAuthorize")
    @ResponseBody
    @PostAuthorize("hasAnyAuthority('menu:system')")
    public String postAuthorize(){
        System.out.println("test--PostAuthorize");
        return "PostAuthorize";
    }

使用 不同的 用户进行测试

@PostFilter

@PostFilter :权限验证之后对数据进行过滤 留下用户名是 admin1 的数据 表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素

java
  @RequestMapping("/testPostFilter")
    @PreAuthorize("hasRole('ROLE_管理员')")
    @PostFilter("filterObject.username == 'admin1'")
    @ResponseBody
    public List<Users> getAllUser(){
        ArrayList<Users> list = new ArrayList<>();
        list.add(new Users(1,"admin1","6666"));
        list.add(new Users(2,"admin2","888"));
        return list;
    }

@PreFilter

@PreFilter: 进入控制器之前对数据进行过滤

需要先登录 拿到J Cookie:JSESSIONID

image-20210505172547186

使用postman

image-20210505172609847

image-20210505172630506

json
[
    {
        "id": "1",
        "username": "admin",
        "password": "666"
    },
    {
        "id": "2",
        "username": "admins",
        "password": "888"
    },
    {
        "id": "3",
        "username": "admins11",
        "password": "11888"
    },
    {
        "id": "4",
        "username": "admins22",
        "password": "22888"
    }
]

响应id 为 偶数的

java
 @RequestMapping("/testPreFilter")
    @PreAuthorize("hasRole('ROLE_管理员')")
    @PreFilter(value = "filterObject.id%2==0")
    @ResponseBody
    public List<Users> getTestPreFilter(@RequestBody List<Users>
                                                   list){
        list.forEach(t-> {
            System.out.println(t.getId()+"\t"+t.getUsername());
        });
        return list;
    }

8.10 记住我

创建表

sql
CREATE TABLE `persistent_logins` (
 `username` VARCHAR(64) NOT NULL,
 `series` VARCHAR(64) NOT NULL,
 `token` VARCHAR(64) NOT NULL,
 `last_used` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
CURRENT_TIMESTAMP,
 PRIMARY KEY (`series`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

配置文件配置数据源

yml
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://127.0.0.1:3306/springsecurity?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
    driver-class-name: com.mysql.cj.jdbc.Driver

编写配置类

java
package com.glls.springsecuritydemo3.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;


@Configuration
public class BrowserSecurityConfig {

    @Autowired
    private DataSource dataSource;
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 赋值数据源
        jdbcTokenRepository.setDataSource(dataSource);
// 自动创建表,第一次执行会创建,以后要执行就要删除掉!
       // jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }


}

修改安全配置类

java
package com.glls.springsecuritydemo3.config;

import org.springframework.beans.factory.annotation.Autowired;
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.builders.HttpSecurity;
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;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PersistentTokenRepository tokenRepository;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.formLogin()  // 自定义登录页面
                .loginPage("/login.html")  // 指定登录页面路径  如果直接是页面  必须在static中
                //.loginPage("/index")  // 指定登录页面路径  通过controller 跳转  则在templates 中
                .loginProcessingUrl("/login")
                //.successForwardUrl("/main")  // 登录页面表单的请求路径
                .defaultSuccessUrl("/main").permitAll() // 登陆成功之后 默认 跳转路径
                //.failureForwardUrl("/fail")    // 登录失败跳转到哪个url

                .usernameParameter("myusername")
                .passwordParameter("mypassword")
                .and()
                .authorizeRequests()
                .antMatchers("/layui/**","/user/anno") //表示配置请求路径
                .permitAll() // 指定 URL 无需保护。
                //1.hasAuthority方法:当前登陆用户,只有具有admin权限才可以访问这个路径
                //.antMatchers("/user/findAll").hasAuthority("menu:system")
                //2.hasAnyAuthority方法:当前登陆用户,具有admin或manager权限可以访问这个路径
                //.antMatchers("/user/findAll").hasAnyAuthority("menu:user")
                //3.hasRole方法:当前主体具有指定角色,则允许访问
                .antMatchers("/user/findAll").hasRole("管理员")
                //4.hasAnyRole方法:当前主体只要具备其中某一个角色就能访问
                //.antMatchers("/user/findAll").hasAnyRole("管理员,普通用户")
                .anyRequest() // 其他请求
                .authenticated() //需要认证
                .and().csrf().disable(); //关闭csrf防护  这个一定要加上

        http.exceptionHandling().accessDeniedPage("/unauth");

        // 开启记住我功能
        http.rememberMe()
                .tokenRepository(tokenRepository)
                .userDetailsService(userDetailsService);

    }


    // 注入 PasswordEncoder 类到 spring 容器中
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

页面添加记住我复选框

html
<form action="/login"  method="post">
   
    <input type="text" name="myusername" /> <br>
    <input type="password" name="mypassword" /> <br>
    记住我:<input type="checkbox"name="remember-me"title="记住密码"/><br/>
    <input type="submit" value="提交"/>
</form>
  • 此处:name 属性值必须位 remember-me.不能改为其他值

使用张三进行登录测试 登录成功之后,关闭浏览器再次访问 http://localhost:8080/user/findAll,发现依然可以使用!

  • 设置有效期 默认 2 周时间。但是可以通过设置状态有效时间,即使项目重新启动下次也可以正常登 录。 在配置文件中设置
java
 http.rememberMe()
                .tokenValiditySeconds(60)   // 单位是秒
                .tokenRepository(tokenRepository)
                .userDetailsService(userDetailsService);

8.11 注销

配置类中添加退出映射地址

java
 http.logout().logoutUrl("/logout") //注销时访问的路径
                .logoutSuccessUrl("/login.html").permitAll(); //注销成功后访问的路径

九.CSRF

CSRF 理解 跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已 登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。 跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个 自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买 商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。 这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的 浏览器,却不能保证请求本身是用户自愿发出的。 从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用 程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。

实现步骤

在登录页面添加一个隐藏域:

html
<input 
type="hidden"th:if="${_csrf}!=null"th:value="${_csrf.token}"name="_csrf
"/>

关闭安全配置的类中的 csrf

java
// http.csrf().disable();

10. Security + Jwt + vue-admin-template

在前后端分离的应用中 security的使用 ,咱们得使用方式 是 前端发送登录请求 ,提交用户名和密码 后端接口接受以后 把认证流程交给security 来完成 , 认证之后 做授权操作。

使用流程:

  • 认证

    • java
      //认证管理器做认证
      Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
  • 把认证之后的用户信息 存入 redis ,并且向前端返回 token

    • java
      Token token = tokenService.saveToken(loginUser);

细节:

前端:发送登录请求,后端接口接受

java
// UserController  
@PostMapping("/login")
    @UnInterception
    public R login(@RequestBody LoginUser loginUser){
        //登录传过来的用户名 密码 数据
        log.info(loginUser.toString());
        String token = loginService.login(loginUser.getUsername(),loginUser.getPassword());
        return R.ok().code(20000).data("token",token);
    }

LoginUser 是咱们自己封装的用户主体对象 实现了 UserDetails 接口

java
package com.glls.phoneservice.security.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;

/**
 * @date 2023/6/8
 * @desc  实现 UserDetails
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails, Serializable {

    //tokenKey     连接 jwttoken  和 redis 的桥梁
    private String tokenKey;
    // 登陆时间  毫秒
    private Long loginTime;
    // 过期时间   毫秒
    private Long expireTime;


    // 用户id
    private Long userId;
    // 用户名
    private String username;
    // 密码
    private String password;

    public LoginUser(Long userId, String username, String password, List<SimpleGrantedAuthority> authorities) {
        this.userId = userId;
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }

    /**
     * 权限列表
     * */

    private List<SimpleGrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }
    //账号是否未过期
    @Override
    @JsonIgnore
    public boolean isAccountNonExpired() {
        return true;
    }
    //账号是否未锁定
    @Override
    @JsonIgnore
    public boolean isAccountNonLocked() {
        return true;
    }
    // 密码是否未过期
    @Override
    @JsonIgnore
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 账号是否可用
    @Override
    @JsonIgnore
    public boolean isEnabled() {
        return true;
    }
}

把认证操作交给 loginService 来完成 同时 返回 token ,loginService 借助 认证管理器 authenticationManager 把认证操作交给security 来完成 ,返回 Authentication 认证信息对象 ,认证成功后 从这个对象 可以拿到 认证主体 LoginUser 因为 UserDetailsService 的 loadUserByUsername 方法的返回值 就是 LoginUser

java
package com.glls.phoneservice.security.service;

import com.glls.phoneservice.security.dto.Token;
import com.glls.phoneservice.security.model.LoginUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;

import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * @date 2023/6/8
 * @desc 处理登录 对接security
 */
@Component
public class LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Resource
    private TokenService tokenService;
    //走security 的登录流程  返回 token
    public String login(String username, String password) {
        // 认证管理 认证  返回认证对象  authenticate 就是认证流程
        Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        // 认证对象获取主题信息   就是 UserDetails  LoginUser 是它的实现类
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();

        //返回 token   这个 token  应该封装什么??
        Token token = tokenService.saveToken(loginUser);

        return token.getToken();
    }
}

把认证成功后的用户信息 存入 redis 并向前端返回 token --------> tokenService.saveToken(loginUser);

java
public interface TokenService {

    // 生成token   把用户信息存入 redis
    public Token saveToken(LoginUser loginUser);

    //根据token 得到用户信息
    public LoginUser getLoginUser(String jwtToken);
    
    // 刷新token
    public void refresh(LoginUser loginUser);
    // 删除token
    public boolean deleteToken(String token);

}

实现类 TokenServiceImpl

java
@Override
    public Token saveToken(LoginUser loginUser) {

        // 设置tokenKey
        loginUser.setTokenKey(UUID.randomUUID().toString());

        // 把用户信息存入 redis
        cacheLoginUser(loginUser);

        // 根据 tokenKey s生成token
        String jwtToken = createJWTToken(loginUser);

        return new Token(jwtToken, loginUser.getLoginTime());
    }

TokenServiceImpl

java
package com.glls.phoneservice.security.service.impl;

import com.alibaba.fastjson.JSON;
import com.glls.phoneservice.security.dto.Token;
import com.glls.phoneservice.security.model.LoginUser;
import com.glls.phoneservice.security.service.TokenService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @date 2023/6/8
 * @desc
 */
@Service("tokenService")
@Slf4j
@Primary
public class TokenServiceImpl implements TokenService {

    /**
     * token过期秒数
     */
    @Value("${token.expire.seconds}")
    private Integer expireSeconds;

    private static final String LOGIN_USER_KEY = "LOGIN_USER_KEY";


    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static Key KEY = null;


    /**
     * 私钥
     */
    @Value("${token.jwtSecret}")
    private String jwtSecret;



    @Override
    public Token saveToken(LoginUser loginUser) {

        // 设置tokenKey
        loginUser.setTokenKey(UUID.randomUUID().toString());

        // 把用户信息存入 redis
        cacheLoginUser(loginUser);

        // 根据 tokenKey s生成token
        String jwtToken = createJWTToken(loginUser);

        return new Token(jwtToken, loginUser.getLoginTime());
    }



    //解析token  获取 redis 中的 key  ,根据 key 获取用户信息

    @Override
    public LoginUser getLoginUser(String jwtToken) {

        //从jwt中获取用户的基本信息
        String uuid = getUUIDFromJWT(jwtToken);

        if (uuid != null) {
            //redis:缓存服务器,暂时存放数据,效率比数据库高的键值对数据库。
            //从redis中获取完整的用户数据
            String s = stringRedisTemplate.boundValueOps(getTokenKey(uuid)).get();
            LoginUser loginUser = JSON.parseObject(s, LoginUser.class);
            return loginUser;
        }
        return null;
    }

    // 删除 token
    @Override
    public boolean deleteToken(String token) {
        String uuid = getUUIDFromJWT(token);
        if (uuid != null) {
            String key = getTokenKey(uuid);
            String loginUser = stringRedisTemplate.opsForValue().get(key);
            if (loginUser != null) {
                stringRedisTemplate.delete(key);
                return true;
            }
        }
        return false;
    }


    //刷新token
    @Override
    public void refresh(LoginUser loginUser) {
        cacheLoginUser(loginUser);
    }


    //解析token
    private String getUUIDFromJWT(String jwtToken) {
        if ("null".equals(jwtToken) || StringUtils.isBlank(jwtToken)) {
            return null;
        }

        try {
            Claims body = Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(jwtToken).getBody();
            return body.get(LOGIN_USER_KEY,String.class);

        } catch (ExpiredJwtException e) {
            log.error("{}已过期", jwtToken);
        } catch (Exception e) {
            log.error("{}", e);
        }

        return null;
    }


    /**
     * 生成jwt
     *
     * @param loginUser
     * @return
     */
    private String createJWTToken(LoginUser loginUser) {

        //自定义准备封装到token 中的数据
        Map<String, Object> claims = new HashMap<>();

        claims.put(LOGIN_USER_KEY, loginUser.getTokenKey());// 放入一个随机字符串,通过该串可找到登陆用户

        String jwtToken = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS256, getKeyInstance())
                .compact();

        return jwtToken;
    }

    private Key getKeyInstance() {
        if (KEY == null) {
            synchronized (TokenServiceImpl.class) {
                if (KEY == null) {// 双重锁
                    byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(jwtSecret);
                    KEY = new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS256.getJcaName());
                }
            }
        }
        return KEY;
    }

    //把认证之后的数据存入redis
    private void cacheLoginUser(LoginUser loginUser) {

        //设置登陆时间
        loginUser.setLoginTime(System.currentTimeMillis());
        // 设置过期时间
        loginUser.setExpireTime(loginUser.getLoginTime() + expireSeconds * 1000);
        // 根据uuid将loginUser缓存
        //redisTemplate.boundValueOps(getTokenKey(securityUser.getToken())).set(securityUser, expireSeconds, TimeUnit.SECONDS);

        stringRedisTemplate.opsForValue().set(getTokenKey(loginUser.getTokenKey()), JSON.toJSONString(loginUser),expireSeconds, TimeUnit.SECONDS);
    }


    private String getTokenKey(String uuid) {
        return "tokens:" + uuid;
    }

}

注意 security 核心配置类 必须进行配置 配置了 认证管理器 密码加密器 校验token 的 过滤器 退出操作的处理器

java
package com.glls.phoneservice.security.config;

import com.glls.phoneservice.security.filter.TokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

/**
 * @date 2023/7/25
 * @desc
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 注入 PasswordEncoder 类到 spring 容器中
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //使用 咱们 指定的  userDetailsService  和   passwordEncoder
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

    // 注销 退出  处理类
    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;


    // 这个过滤器   自定义的过滤器  作用 就是 拦截请求  解析token
    @Autowired
    private TokenFilter tokenFilter;


    // 白名单    anonymous() 允许匿名用户访问,不允许已登入用户访问
    private static final String[] WHITELIST_URL = {
            //"/phoneservice/user/login",
            "/user/login",
            "/user/register",
            "/user/checkPhone/*",
            //"/phone/index",
            //"/phone/findByCategoryType/*",
            //"/phone/findSpecsByPhoneId/*",
            "/phoneservice/user/sendCheckCode",
            "/captchaImage",
            "/v2/**","/doc.html","/swagger/**","/webjars/**","/swagger-resources/**"};
    // 白名单   不管登入,不登入 都能访问
    private static final String[] WHITELIST_SOURCE = {"/**/*.css", "/webjars/**"};

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //退出登录状态
        http.logout().logoutUrl("/phoneservice/user/logout").logoutSuccessHandler(logoutSuccessHandler);

        http
                //这一块配置   被全局异常处理器替换掉了   全局异常处理器 优先级高一点
                //.exceptionHandling()   // 异常处理
                //.authenticationEntryPoint(authenticationEntryPoint)   // 处理 未认证 或 密码错误
                //.accessDeniedHandler(accessDeniedHandler) // 处理权限不足
                //.and()
                .authorizeRequests()
                  //anonymous() 允许匿名用户访问,不允许已登入用户访问
                .antMatchers(WHITELIST_URL).anonymous()    //  匿名访问的请求  不能携带token ,是匿名  就必须是匿名
                //.antMatchers(WHITELIST_URL).permitAll()
                //.antMatchers("/","layui/**","/user/anno") //配置无需认证的请求路径
                .antMatchers(HttpMethod.GET, WHITELIST_SOURCE) //配置无需认证的请求路径
                .permitAll() // 指定 URL 无需保护。    不管登入,不登入 都能访问
                .anyRequest() // 其他请求
                .authenticated() //需要认证

                .and().csrf().disable(); //关闭csrf防护  这个一定要加上

        // 设置响应头   默认的安全头
        http.headers().cacheControl();

        //配置自定义过滤器
        http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);

    }

}

在 认证 和 授权中 出现的所有问题 比如 认证失败 或者 无权限访问 security 都会 抛出异常 , 在这里 咱们 使用 全局异常处理器 来处理这些异常‘

java
/**
     * 密码错误
     * @param ex BadCredentialsException
     * @return
     *
     * 注意  这个 全局异常处理器 的优先级  高于  security 的  一些 handler
     */
    @ExceptionHandler(BadCredentialsException.class)
    public R handleBadCredentialsException(
            BadCredentialsException ex) {
        log.error("密码错误,{}",ex.getMessage());
        return R.setResult(ResultCodeEnum.PASSWORD_ERROR);
    }

    /**
     * 无权限访问
     * @param ex AccessDeniedException
     * @return
     *
     * 注意  这个 全局异常处理器 的优先级  高于  security 的  一些 handler
     */
    @ExceptionHandler(AccessDeniedException.class)
    public R handleAccessDeniedException(
            AccessDeniedException ex) {
        log.error("无权限访问,{}",ex.getMessage());
        return R.setResult(ResultCodeEnum.ACCESS_DENIED).code(20000);
    }

校验token 的 过滤器

java
package com.glls.phoneservice.security.filter;

import com.glls.phoneservice.security.model.LoginUser;
import com.glls.phoneservice.security.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @date 2023/6/8
 * @desc
 */
@Component
@Slf4j
public class TokenFilter extends OncePerRequestFilter {

    public static final String TOKEN_KEY = "token";


    private static final Long MINUTES_10 = 10 * 60 * 1000L;


    @Autowired
    private TokenService tokenService;

    @Autowired
    private UserDetailsService userDetailsService;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("校验token的过滤器拦截的路径:"+ request.getRequestURI());

        // 从请求拿到token
        String token = getToken(request);

        if (StringUtils.isNotBlank(token)) {
            //通过JWT解析请求数据中的Token,获取用户数据
            LoginUser loginUser = tokenService.getLoginUser(token);

            if (loginUser != null) {
                // 过期时间与当前时间对比,临近过期10分钟内的话,自动刷新缓存
                loginUser = checkLoginTime(loginUser);
                // 把用户信息 存入security 上下文  后续 授权会用到
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser,
                        null, loginUser.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        filterChain.doFilter(request, response);
    }

    /**
     * 校验时间<br>
     * 过期时间与当前时间对比,临近过期10分钟内的话,自动刷新缓存
     *
     * @param loginUser
     * @return
     */
    private LoginUser checkLoginTime(LoginUser loginUser) {
        //获取过期时间
        long expireTime = loginUser.getExpireTime();
        //获取登陆时间
        long currentTime = System.currentTimeMillis();
        // 如果token 即将过期
        if (expireTime - currentTime <= MINUTES_10) {
            String token = loginUser.getTokenKey();
            //
            loginUser = (LoginUser) userDetailsService.loadUserByUsername(loginUser.getUsername());
            loginUser.setTokenKey(token);
            // 刷新   重新缓存用户信息  并设置过期时间
            tokenService.refresh(loginUser);
        }
        return loginUser;
    }


    /**
     * 根据参数或者header获取token
     *
     * @param request
     * @return
     */
    public static String getToken(HttpServletRequest request) {
        // 请求参数 上 获取 token
        String token = request.getParameter(TOKEN_KEY);
        if (StringUtils.isBlank(token)) {
            // 请求头上  获取 token
            token = request.getHeader(TOKEN_KEY);
        }

        return token;
    }

}

处理 注销成功的 handler

java
package com.glls.phoneservice.security.config;

/**
 * @date 2023/6/8
 * @desc
 */

import com.glls.common.R;
import com.glls.phoneservice.security.filter.TokenFilter;
import com.glls.phoneservice.security.service.TokenService;
import com.glls.phoneservice.utils.ResponseUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @date 2022/8/24
 * @desc
 *
 * 当有全局异常处理器的时候  全局异常处理器的优先级 高于下面的 handler
 */
@Configuration
public class SecurityHandlerConfig {

    @Autowired
    private TokenService tokenService;

    /**
     * 退出处理
     *
     * @return
     */
    @Bean
    public LogoutSuccessHandler logoutSuccessHandler() {
        return new LogoutSuccessHandler() {

            @Override
            public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
                R ok = R.ok().code(20000).message("退出成功");
                // TokenFilter
                String token = TokenFilter.getToken(request);
                tokenService.deleteToken(token);

                ResponseUtil.responseJson(response, HttpStatus.OK.value(), ok);
            }
        };

    }
}

Token

java
package com.glls.phoneservice.security.dto;

import java.io.Serializable;

/**
 * @date 2022/12/8
 * @desc
 */
public class Token implements Serializable {
    private static final long serialVersionUID = 6314027741784310221L;

    // 字符串 token   一个加密的字符串
    private String token;
    /** 登陆时间戳(毫秒) */
    private Long loginTime;

    public Token(String token, Long loginTime) {
        super();
        this.token = token;
        this.loginTime = loginTime;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public Long getLoginTime() {
        return loginTime;
    }

    public void setLoginTime(Long loginTime) {
        this.loginTime = loginTime;
    }


}

UserDetailService

java
package com.glls.phoneservice.security;

import com.glls.common.pojo.User;
import com.glls.phoneservice.security.model.LoginUser;
import com.glls.phoneservice.service.MenuService;
import com.glls.phoneservice.service.RoleService;
import com.glls.phoneservice.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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;
import java.util.Set;

/**
 * @date 2023/7/25
 * @desc
 */
@Service("userDetailsService")
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private MenuService menuService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名 查询用户信息
        User user = userService.findUserByUsername(username);
        if(user==null){
            throw new UsernameNotFoundException("用户名不存在");
        }
        // 查询 该用户的 角色 权限信息  封装到一个 集合
        List<SimpleGrantedAuthority>  authorities = getUserAuthorities(user.getId());
        return new LoginUser(user.getId(),user.getUsername(),user.getPassword(),authorities);
    }

    private List<SimpleGrantedAuthority> getUserAuthorities(Long id) {

        ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();

        //查询角色
        Set<String> roles = roleService.getRoleByUserId(id);
        
        if(roles.size()>0){
            for(String role :roles){
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + role);
                authorities.add(simpleGrantedAuthority);
            }
        }
        
        //查询权限
        Set<String> permissions = menuService.getPermissionByUserId(id);
        if(permissions.size()>0){
            for(String perm :permissions){
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(perm);
                authorities.add(simpleGrantedAuthority);
            }
        }

        return authorities;
    }
}

vue-admin-template

登录请求 login ---- 后端返回 token

获取用户信息 getInfo ---- 后端返回用户 身份 角色权限