Spring Security学习笔记(二)Spring Security认证和鉴权

news2024/12/24 9:42:58

前言:本系列博客基于Spring Boot 2.6.x依赖的Spring Security5.6.x版本

上一篇博客介绍了Spring Security的整体架构,本篇博客要讲的是Spring Security的认证和鉴权两个重要的机制。
在这里插入图片描述
UsernamePasswordAuthenticationFilter和BasicAuthenticationFilter是用来认证的两个过滤器,FilterSecurityInterceptor是用来鉴权的。

一、Spring Security认证

Spring Security提供了许多认证机制,例如用户名密码认证、OAuth 2.0认证、SAML认证、Central Authentication Server (CAS)认证、Remember Me(记住过了session有效期的用户)、JAAS认证、X509认证等

1.1、认证架构

Spring Security认证架构主要由以下几个组件构成:
SecurityContext:Spring Security的上下文对象,包含了当前认证用户的Authentication(认证)。

SecurityContextHolder:用于设置和获取SecurityContext的静态工具类,保存了SecurityContext上下文对象。

Authentication:认证接口,定义了获取用户凭证、认证信息、权限等方法规范。

GrantedAuthority:权限类,用来定义用户的权限,Authentication中会保存一个GrantedAuthority类型的权限列表。

AuthenticationManager:认证管理器接口,只有一个authenticate方法,它的实现类实现该方法用来执行具体的认证逻辑,入参和出参都是Authentication。

ProviderManager:最常见的AuthenticationManager的实现。

AuthenticationProvider:认证功能提供者接口。在ProviderManager中实际上的认证逻辑由该接口的实现类处理。DaoAuthenticationProvider、AnonymousAuthenticationProvider都是它的实现类。

AuthenticationEntryPoint:用于从客户端请求凭证(即重定向到登录页面,返回需要登录响应等)。

AbstractAuthenticationProcessingFilter:一个用于认证的基本 Filter。是一个抽象类,只有UsernamePasswordAuthenticationFilter一个实现,UsernamePasswordAuthenticationFilter会从请求中获取username和 password参数,去进行认证。

1.1.1、SecurityContext

Spring Security的上下文对象,可以设置和获取Authentication认证信息。

public interface SecurityContext extends Serializable {
	// 获取Authentication对象
    Authentication getAuthentication();
    // 放入Authentication对象
    void setAuthentication(Authentication authentication);
}

1.1.2、SecurityContextHolder

SecurityContextHolder是用来设置和获取SecurityContext的静态工具类,SecurityContextHolder不关心SecurityContext里认证信息的细节,即Authentication的具体实现类型是什么它并不关心,如果它能获取到值,这个值就认为是当前用户的认证信息。

public class SecurityContextHolder {
	...
	//常用方法
    public static void clearContext() {
        strategy.clearContext();
    }
    
    public static SecurityContext getContext() {
        return strategy.getContext();
    }

    public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    }
    ...
}

SecurityContextHolder架构图:
在这里插入图片描述
默认情况下,SecurityContextHolder使用ThreadLocal来存储这些细节,这意味着 SecurityContext 对同一线程中的方法总是可用的,即使SecurityContext没有被明确地作为参数传递给这些方法。并且Spring Security的FilterChainProxy会确保SecurityContext总是被清空,不用我们手动清空。

1.1.3、Authentication

Authentication是认证信息接口,定义了获取用户凭证、认证信息、权限等方法规范。它主要有两个作用,一是充当未认证的用户凭证(包括用户名、密码);一是表示验证后的认证信息(包括认证后用户信息、用户权限等)。Authentication一般包含了如下信息:

principal: 识别用户。当用用户名/密码进行认证时,这通常是 UserDetails 的一个实例。

credentials: 通常是一个密码。在许多情况下,这在用户被认证后被清除,以确保它不会被泄露。

authorities: GrantedAuthority 实例是用户被授予的权限。

public interface Authentication extends Principal, Serializable {
	//获取用户权限,一般情况下获取到的是用户的角色信息
    Collection<? extends GrantedAuthority> getAuthorities();
    //获取证明用户认证的信息,通常情况下获取到的是密码等信息
    Object getCredentials();
    //获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)
    Object getDetails();
    // 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails
    Object getPrincipal();
    //获取当前 Authentication 是否已认证
    boolean isAuthenticated();
    //设置当前 Authentication 是否已认证(true or false)
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

1.1.4、GrantedAuthority

Spring Security定义的权限类规范接口,认证后的用户权限就是以GrantedAuthority类型的集合保存的。使用时分两种权限,分别是角色(role)和作用域(scope)。role类型的权限表示该权限为角色,角色可能会对应许多的具体资源(菜单、接口等)权限;scope表示某个具体资源的权限。一般使用role类型的权限,因为使用scope的话,认证时可能会保存有非常多的GrantedAuthority,容易导致内存不足,而role类型基本没有这种问题。注意设置role类型的权限时,权限最好加上ROLE_ 前缀,Spring Security默认的role类型鉴权方法会有ROLE_ 前缀。

public interface GrantedAuthority extends Serializable {
	//拿到权限名
    String getAuthority();
}

1.1.5、AuthenticationManager

认证管理器接口,定义了执行认证逻辑的方法API。常用的实现类为ProviderManager。

public interface AuthenticationManager {
	//用户执行认证时的方法,具体逻辑由实现类实现
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

1.1.6 、ProviderManager

ProviderManager是最常用的AuthenticationManager的实现。ProviderManager委托给一个 AuthenticationProvider集合。每个 AuthenticationProvider都有机会表明认证应该是成功的、失败的,或者表明它不能做出决定并允许下游的AuthenticationProvider来决定。如果配置的 AuthenticationProvider实例中没有一个能进行认证,那么认证就会以ProviderNotFoundException 而失败,这是一个特殊的AuthenticationException,表明ProviderManager没有被配置为支持被传入它的Authentication类型。
在这里插入图片描述

1.1.7、AuthenticationProvider

实际上执行认证逻辑的地方。常用的实习类DaoAuthenticationProvider(支持基于用户名/密码的认证)、AnonymousAuthenticationProvider(匿名用户认证)

public interface AuthenticationProvider {
	//执行具体认证逻辑
    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    boolean supports(Class<?> authentication);
}

1.1.8、AuthenticationEntryPoint

如果用户访问一个需要认证后才能访问的资源,AuthenticationEntryPoint就会返回一个响应,需要用户先认证后或者携带认证凭证再访问。比如重定向到登录页面,或者返回一个携带“需要登录”提示的响应信息。我们可以实现该接口,自定义的未登录认证提示。Spring Security默认会对未认证去访问需要认证的资源的请求返回403。

1.1.9、AbstractAuthenticationProcessingFilter

用户认证的基础Filter,只有UsernamePasswordAuthenticationFilter这一个实现类。

AbstractAuthenticationProcessingFilter源码:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
   ...
   //主要方法
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
    }

    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    	//先校验请求url与表单校验提交的url是否一致,不一致执行下一个Filter
    	//一致的话就执行认证逻辑,一般默认的表单提交url是"/login"
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            try {
            	//实现类执行具体的认证逻辑
                Authentication authenticationResult = this.attemptAuthentication(request, response);
                if (authenticationResult == null) {
                    return;
                }

                this.sessionStrategy.onAuthentication(authenticationResult, request, response);
                if (this.continueChainBeforeSuccessfulAuthentication) {
                    chain.doFilter(request, response);
                }

                this.successfulAuthentication(request, response, chain, authenticationResult);
            } catch (InternalAuthenticationServiceException var5) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var5);
                this.unsuccessfulAuthentication(request, response, var5);
            } catch (AuthenticationException var6) {
                this.unsuccessfulAuthentication(request, response, var6);
            }

        }
    }
    //由子类实现具体的验证逻辑
    public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException;

	...
}

UsernamePasswordAuthenticationFilter的 attemptAuthentication() 方法

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
        	//取用户名,实际上是从request取username参数
            String username = this.obtainUsername(request);
            username = username != null ? username : "";
            username = username.trim();
            //取密码,实际上是从request取password参数
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

在这里插入图片描述
AbstractAuthenticationProcessingFilter认证步骤:
1、当用户提交他们的凭证(用户名和密码)时,AbstractAuthenticationProcessingFilter会从HttpServletRequest中创建一个要认证的Authentication。创建的认证的类型取决于 AbstractAuthenticationProcessingFilter的子类。例如,UsernamePasswordAuthenticationFilter从HttpServletRequest中提交的username和password创建一个 UsernamePasswordAuthenticationToken。

2、接下来,Authentication被传入AuthenticationManager,执行认证逻辑。

3、如果认证失败,则为Failure。

  • SecurityContextHolder被清空。

  • RememberMeServices.loginFail被调用。如果没有配置记住我(remember me),可以忽略。

  • AuthenticationFailureHandler被调用。参考AuthenticationFailureHandler接口。

4、 如果认证成功,则为Success。

  • SessionAuthenticationStrategy被通知有新的登录。参考SessionAuthenticationStrategy接口。

  • Authentication是在SecurityContextHolder上设置的。如果你需要保存SecurityContext以便在未来的请求中自动设置,必须显式调用SecurityContextRepository#saveContext。参考 SecurityContextHolderFilter类。

  • RememberMeServices.loginSuccess 被调用。如果没有配置remember me,可以忽略。

  • ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent事件。

  • AuthenticationSuccessHandler被调用。参考AuthenticationSuccessHandler接口。

1.2、代码示例

1.2.1、默认登录认证

引入需要用到的相关包。

	<dependencies>
		<!-- 如果你项目的maven父工程是spring-boot-starter-parent包,可以不写版本号,springboot管理了版本号-->
		<!--Spring Security-->
        <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>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--使用undertow容器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
        
		<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
		<dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
	</dependencies>

定义一个controller

@Controller
public class LoginController {
	//主页url
    @RequestMapping("/main")
    public String mainPage(){
        return "main";
    }
}

resource/templates/ 路径下里定义一个main.html作为主页

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>主页面</title>
</head>
<body>
    <h1>主页面</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="登出">
    </form>
</body>
</html>

application.yml

server:
  port: 8084
  servlet:
    context-path: /security
    
spring:
  security:
  	#配置Spring Security默认登录用户和密码
  	#不配置的话,启动项目时,Spring Security会在控制台打印出默认密码,用户名是User
    user:
      name: User
      password: 123456

一切准备就绪,启动项目,访问localhost:8084/security/main,会自动重定向到Spring Security的默认登录页面。
在这里插入图片描述
这是因为Spring Security使用了默认的表单登录认证的方式。查看控制台打印信息,可以看到类似下面的输出。
如果没有,可能是Spring Security的版本问题,我使用的Spring Boot-2.6.2引入的Spring Security-5.6.2,关于这一块的打印信息逻辑写错了,导致未打印,可以将Spring Boot版本升级一下。

2023-06-14T08:55:22.321-03:00  INFO 76975 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]

UsernamePasswordAuthenticationFilter过滤器就是用来表单登录认证的Filter。

1.2.2、自定义登录页面

resource/templates/ 路径下里定义一个login.html作为登录页

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
    <h1>登录页面</h1>
    <!--method必须为post-->
    <!--th:action="@{/login}",
    使用动态参数,表单中会自动生成_csrf隐藏字段,用于防止csrf攻击
    login:和登录页面保持一致即可,SpringSecurity自动进行登录认证
    /login 是Spring Security默认的登录认证路径,默认情况下用户名和密码名称必须是username和password
    -->
    <form th:action="@{/login}"  method="post">
        用户名:<input type="text" name="username"> <br>
        密码:<input type="password" name="password"><br>
        <input type="submit">
    </form>
</body>
</html>

LoginController添加登录页面跳转接口

	@RequestMapping("/myLoginPage")
    public String myLoginPage(){
        return "login";
    }

自定义Spring Security的配置类

@Configuration
public class BasicSecurityConfig {
    
    @Bean
    public SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
        // 登录相关配置
        http.formLogin(formLogin -> formLogin
                .loginPage("/myLoginPage") // 自定义登录页面,不再使用内置的自动生成页面
                //登录认证接口url,这里可以任意设置,只要保证和登录表单提交的url相同即可
                .loginProcessingUrl("/login")
                .usernameParameter("username")// 表单中的用户名项
                .passwordParameter("password")// 表单中的密码项
                .successForwardUrl("/main")//登录成功后跳转的路径,未设置会跳转到项目根路径
        );
        //设置访问权限,如果不设置,默认所有的url都可以匿名访问
        http.authorizeRequests(authorize ->{authorize
                .antMatchers("/myLoginPage").permitAll() //允许所有用户访问
                .anyRequest()   //对所有请求开启授权保护
                .authenticated(); //已认证的请求会被自动授权
        });
        http.logout(logout ->logout
                .logoutUrl("/logout") //使用该方法时,当开启csrf防护,logout请求必须是post,否则会404
                .clearAuthentication(true) //清除认证状态,默认为true
                .invalidateHttpSession(true) // 销毁HttpSession对象,默认为true
        );
        //关闭csrf防护,否则所有的POST的请求都需要携带CSRF令牌
        http.csrf(csrf -> csrf.disable());
        return http.build(); // 返回构建的SecurityFilterChain实例
    }
}

还有一种写法是继承WebSecurityConfigurerAdapter类,重写configure方法,但是Spring Security 6.0及之后的版本删除了WebSecurityConfigurerAdapter类,不能用这种写法配置了。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    public void configure(HttpSecurity http) throws Exception {
    	//和上面配置相同,最后无需调用http.build()方法
        ....
    }
}

注意:使用表单登录认证时,实际处理认证的是UsernamePasswordAuthenticationFilter类,loginProcessingUrl方法配置的url可以任意配置,只要和登录表单提交的url相同即可。

1.2.3、自定义Handler逻辑

Spring Security定义了一些Handler接口,让我们可以自定义认证结束后的处理逻辑。比如返回JSON结果,适用于前后端分离的项目。

1.2.3.1、认证成功处理

AuthenticationSuccessHandler类是Spring Security提供的认证成功后处理逻辑接口。
实现AuthenticationSuccessHandler接口:

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        //  获取用户身份信息
        UserDetails userDetails = (UserDetails)authentication.getPrincipal();
        //  获取用户的凭证信息
        Object credentials = authentication.getCredentials();
        //  获取用户权限信息
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        /*返回页面,适用于前后端未分离的项目*/
        System.out.println("用户名:"+userDetails.getUsername());
        System.out.println("一些操作...");
        //response.sendRedirect(request.getContextPath()+"/main");

        /*返回json,适用于前后端分离*/
        //这里可以生成token,并存redis等
        Map<String,Object> result = new HashMap();
        result.put("code",0);   // 成功
        result.put("message","登录成功");   //
        result.put("data",userDetails);   //这里可以换成token,jwt等登录成功凭证
        //  将结果对象转换成json字符串
        String json = JSON.toJSONString(result);
        //  返回json数据到前端
        //  响应头
       response.setContentType("application/json;charset=UTF-8");
        //  响应体
        response.getWriter().println(json);
    }
}

在BasicSecurityConfig配置类的formLogin中加上MyAuthenticationSuccessHandler

		// 登录相关配置
        http.formLogin(formLogin -> formLogin
                .loginPage("/myLoginPage") // 自定义登录页面,不再使用内置的自动生成页面
                //登录认证接口url,这里可以任意设置,只要保证和登录表单提交的url相同即可
                .loginProcessingUrl("/login")
                .usernameParameter("username")// 表单中的用户名项
                .passwordParameter("password")// 表单中的密码项
                .successForwardUrl("/main")//登录成功后跳转的路径,未设置会跳转到项目根路径
                .successHandler(new MyAuthenticationSuccessHandler()) //认证成功处理
        );
1.2.3.2、认证失败处理

AuthenticationFailureHandler类是Spring Security提供的认证失败处理逻辑接口。
实现AuthenticationFailureHandler接口:

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        //  获取失败的信息
        String localizedMessage = exception.getLocalizedMessage();

        Map<String,Object> result = new HashMap();
        result.put("code",-1);   // 失败
        result.put("message",localizedMessage);   //
        //  将结果对象转换成json字符串
        String json = JSON.toJSONString(result);
        //  返回json数据到前端
        //  响应头
        response.setContentType("application/json;charset=UTF-8");
        //  响应体
        response.getWriter().println(json);

        //重定向到登录错误页面,适用与前后端不分离项目
        //response.sendRedirect(request.getContextPath()+"/loginError");
    }
}

在BasicSecurityConfig配置类的formLogin中加上MyAuthenticationFailureHandler

		// 登录相关配置
        http.formLogin(formLogin -> formLogin
        		...
                .successHandler(new MyAuthenticationSuccessHandler()) //认证成功处理
                .failureHandler(new MyAuthenticationFailureHandler()) //认证失败处理
        );
1.2.3.3、登出成功处理

LogoutSuccessHandler类是Spring Security提供的登出成功处理逻辑接口。
实现LogoutSuccessHandler接口:

public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //  获取用户身份信息
        UserDetails userDetails = (UserDetails)authentication.getPrincipal();

        /** 返回json,适用于前后端分离*/
        Map<String,Object> result = new HashMap();
        result.put("code",1);   // 成功
        result.put("message","注销成功");   //
        result.put("data",userDetails);   //
        //  将结果对象转换成json字符串
        String json = JSON.toJSONString(result);

        //  返回json数据到前端 适用前后端分离
        //  响应头
        response.setContentType("application/json;charset=UTF-8");
        //  响应体
        response.getWriter().println(json);

        //返回到页面
        //response.sendRedirect(request.getContextPath()+"/main");
    }
}

在BasicSecurityConfig配置类的logout中加上MyLogoutSuccessHandler

http.logout(logout ->logout
                .logoutUrl("/logout") //使用该方法时,当开启csrf防护,logout请求必须是post,否则会404
                .clearAuthentication(true) //清除认证状态,默认为true
                .invalidateHttpSession(true) // 销毁HttpSession对象,默认为true
                .logoutSuccessHandler(new MyLogoutSuccessHandler())
        );
1.2.3.4、请求未认证资源处理

AuthenticationEntryPoint类是Spring Security提供的未认证访问资源处理逻辑接口。
实现AuthenticationEntryPoint类:

public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        String localizedMessage = "需要登录";//authException.getLocalizedMessage();

        Map<String,Object> result = new HashMap();
        result.put("code",-1);   // 告诉用户需要登录
        result.put("message",localizedMessage);   //


        //  将结果对象转换成json字符串
        String json = JSON.toJSONString(result);
        //  返回json数据到前端
        //  响应头
        response.setContentType("application/json;charset=UTF-8");
        //  响应体
        response.getWriter().println(json);
        //返回登录界面
        //response.sendRedirect(request.getContextPath()+"/myLoginPage");
    }
}

在BasicSecurityConfig配置类的中加上配置

		//异常处理
        http.exceptionHandling(exception -> exception
                .authenticationEntryPoint(new MyAuthenticationEntryPoint()) //请求未认证的处理
        );

1.2.4、基于数据库的认证

前面的示例中,我们的登录用户是写在配置文件里的,用的是基于内存存储用户信息的方式。这只能在学习时使用,在实际项目中是不行的。实际项目中,我们的用户信息时存在数据库里的,Spring Security也提供了基于数据库来进行认证的方式。

前文我们已经说过,通过HttpSecurityformLogin方法配置的认证,是使用UsernamePasswordAuthenticationFilter类来进行的认证处理,而实际上处理时,是在ProviderManager的authenticate方法里,再调用DaoAuthenticationProvider的authenticate方法处理的。最终的处理是在DaoAuthenticationProvider类的父类AbstractUserDetailsAuthenticationProvider类的authenticate处理的。

而在进行认证前,需要先根据用户名查询系统里的用户数据(内存或数据库),再根据查询到的用户密码与用户输入的密码校验,校验通过,则认证成功。这一块的逻辑是由DaoAuthenticationProvider类重写父类的retrieveUser实现的。源码如下:

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
        	//拿到用户信息
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }

通过调用UserDetailsService的loadUserByUsername方法,返回系统的用户信息。我们可以通过实现自己的UserDetailsService实现类,重写loadUserByUsername方法,查询数据库里的用户数据。代码如下:


public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
    /**
     * UserDetails提供的字段如果不够的话,可以继承 User类,实现自己的UserDetails
     * 用户认证时会调用
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据userName去数据库查询用户信息, 伪代码
        UserDomain user = userService.queryUserByUserName(username);
        if(user == null){
            throw new UsernameNotFoundException(username);
        }
        //查询用户的角色,伪代码
        List<String> roles = roleService.getRoleCodeByUserId(user.getId());
        
        UserDetails userDetails = User.withUsername(user.getLoginName())
                .password(user.getPassword())
                //.authorities(roles.toArray(new String[roles.size()]))  //权限,和roles配一个就行,这里配置不会加前缀
                .roles(roles.toArray(new String[roles.size()])) //角色 配置角色时,会给资源自动加上ROLE_前缀
                .build();
        return userDetails;
    }

    @Override
    public void createUser(UserDetails user) {

    }

    @Override
    public void updateUser(UserDetails user) {

    }

    @Override
    public void deleteUser(String username) {

    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {

    }

    @Override
    public boolean userExists(String username) {
        return false;
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        return null;
    }
}

然后在配置类中加上相关配置:

@Configuration
public class BasicSecurityConfig {
	...
	
	/**
     * 密码编码器,会对请求传入的密码进行加密
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        //return NoOpPasswordEncoder.getInstance();
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(){
        return new DBUserDetailsManager();
    }
    
    ...
}	

需要加一个密码编码器,使用Spring Security提供的默认编码器就行,使用编码器后,注意数据库保存的密码应该是密文。直接将我们的UserDetailsService注入到Spring容器中即可生效。

二、Spring Security鉴权

2.1、鉴权架构

2.1.1、FilterSecurityInterceptor

Spring Security进行鉴权处理的入口。父类是AbstractSecurityInterceptor类

2.1.2、AccessDecisionManager

Spring Security鉴权的真正处理者

public interface AccessDecisionManager {
	//鉴权方法 
	/**
	* authentication 当前用户的认证凭证信息,包括了用户信息,权限等
	* object 一般是FilterInvocation,包含了当前请求的request和response
	* configAttributes过滤规则,由配置类里的 HttpSecurity的authorizeRequests方法配置
	*/
    void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;
    //是否允许AccessDecisionManager处理该过滤规则,true为允许
    boolean supports(ConfigAttribute attribute);
	//是否允许AccessDecisionManager处理clazz类型,true为允许
    boolean supports(Class<?> clazz);
}

Spring Security的鉴权是基于投票机制的鉴权方式。
在这里插入图片描述

2.1.3、AccessDecisionVoter

投票器,AccessDecisionManager的投票处理是由AccessDecisionVoter投票器决定的,一个AccessDecisionManager里会包含一个AccessDecisionVoter集合,AccessDecisionManager会根据所有投票器的投票结果来决定请求是否有权访问,无权限会抛出一个 AccessDeniedException。

public interface AccessDecisionVoter<S> {
	//同意
    int ACCESS_GRANTED = 1;
    //弃权
    int ACCESS_ABSTAIN = 0;
    //反对
    int ACCESS_DENIED = -1;

    boolean supports(ConfigAttribute attribute);
	
    boolean supports(Class<?> clazz);
    //投票方法
    int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}

AccessDecisionManager有三个实现类

  • AffirmativeBased:一票通过,只要有一票通过就算通过,默认是它。
  • UnanimousBased:一票反对,只要有一票反对就不能通过。
  • ConsensusBased:少数票服从多数票。

2.2、代码示例

2.2.1、默认鉴权

定义两个接口,分别由两种权限访问。在LoginController中新增

	//admin权限
	@RequestMapping("/adminRole")
    @ResponseBody
    public String adminRole(){
        return "success";
    }
	//tourist权限
    @RequestMapping("/touristRole")
    @ResponseBody
    public String touristRole(){
        return "success";
    }

在BasicSecurityConfig配置类中新增这两个接口鉴权配置:

	@Bean
    public SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
    	//和前文一样的配置省略了
        ...
        //设置访问权限,如果不设置,默认所有的url都可以匿名访问
        http.authorizeRequests(authorize ->{authorize
                // 放行所有OPTIONS请求
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .antMatchers("/myLoginPage").permitAll() //登录页面允许所有用户访问
                .antMatchers("/adminRole").hasRole("AdminManager")	// /adminRole 只能AdminManager角色访问
                .antMatchers("/touristRole").hasAnyRole("AdminManager","ApproveUser")	// /touristRole AdminManager和ApproveUser角色都能访问
                .anyRequest()   //对所有请求开启授权保护
                .authenticated(); //已认证的请求会被自动授权
        });
       	...
        return http.build(); // 返回构建的SecurityFilterChain实例
    }

通过给“/adminRole”和"/touristRole"接口配置权限过滤规则,用户访问接口时,就会在登录认证成功后,在SecurityContext上下文中设置凭证信息,其中就包括当前用户的权限,然后匹配配置的权限过滤规则,判断当前用户是否有该接口的权限。如果不配置权限过滤规则,则默认认证成功的用户都可以访问。

前文说过,在进行表单登录认证时,Spring Security是通过调用UserDetailsService的loadUserByUsername方法,得到当前登录用户的信息的,其中就包括权限信息。

 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据userName去数据库查询用户信息, 伪代码
        UserDomain user = userService.queryUserByUserName(username);
        if(user == null){
            throw new UsernameNotFoundException(username);
        }
        //查询用户的角色,伪代码
        List<String> roles = roleService.getRoleCodeByUserId(user.getId());
        
        UserDetails userDetails = User.withUsername(user.getLoginName())
                .password(user.getPassword())
                //.authorities(roles.toArray(new String[roles.size()]))  //权限,和roles配一个就行,这里配置不会加前缀
                .roles(roles.toArray(new String[roles.size()])) //角色 配置角色时,会给资源自动加上ROLE_前缀
                .build();
        return userDetails;
    }

通过Spring Security的User类的roles和authorities方法,就可以设置当前登录用户的权限信息。这里需要注意的是,如果配置权限过滤规则时,使用的是role(角色)权限,loadUserByUsername方法也得设置role权限,反之亦然。权限名称相同即可。

2.2.2、请求未授权接口处理

Spring Security定义了AccessDeniedHandler接口,用来处理访问未授权接口的请求。只需实现AccessDeniedHandler接口,然后将自定义的类加入到配置里即可。

public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Map<String,Object> result = new HashMap();
        result.put("code",-1);   // 没有权限
        result.put("message","没有权限");   //


        //  将结果对象转换成json字符串
        String json = JSON.toJSONString(result);

        //  返回json数据到前端
        //  响应头
        response.setContentType("application/json;charset=UTF-8");
        //  响应体
        response.getWriter().println(json);
        //返回页面
        //response.sendRedirect(request.getContextPath()+"/main");
    }
}

在BasicSecurityConfig配置类中加上该类

	@Bean
    public SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
       ...
        //异常处理
        http.exceptionHandling(exception -> exception
                .authenticationEntryPoint(new MyAuthenticationEntryPoint()) //请求未认证的处理
                .accessDeniedHandler(new MyAccessDeniedHandler())   //未授权处理
        );
        //关闭csrf防护,否则所有的POST的请求都需要携带CSRF令牌
        http.csrf(csrf -> csrf.disable());
        return http.build(); // 返回构建的SecurityFilterChain实例
    }

2.2.3、基于方法注解的方式鉴权

Spring Security提供了基于注解的方式,设置权限过滤规则的方法。具体使用如下:
使用@EnableMethodSecurity注解开启基于方法的授权,在自定义的BasicSecurityConfig配置类上加上即可

@Configuration
@EnableMethodSecurity
public class BasicSecurityConfig {
	...
}

然后在Controller的方法上使用@PreAuthorize注解即可。首先在配置类里去掉"/adminRole"和"/touristRole"的权限过滤规则配置。然后在LoginController里给这两个接口加上@PreAuthorize注解:

	@RequestMapping("/adminRole")
    @ResponseBody
    @PreAuthorize("hasAnyRole('AdminManager')")
    public String adminRole(){
        return "success";
    }

    @RequestMapping("/touristRole")
    @ResponseBody
    @PreAuthorize("hasAnyRole('AdminManager','ApproveUser')")
    public String touristRole(){
        return "success";
    }

@PreAuthorize里可以使用SpEL表达式,例如:hasRole(‘ADMIN’) and authentication.name == ‘User’ 这种。可以使用的规则如下:
在这里插入图片描述
具体可以参考Spring Security关于这一块的官网介绍:

https://springdoc.cn/spring-security/servlet/authorization/authorize-http-requests.html#authorization-expressions

相类似的注解还有@PostAuthorize、@PreFilter、@PostFilter等。具体的用法也可以去官网查找。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1951589.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++:平衡搜索二叉树(AVL)

hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起学习《C&#xff1a;平衡搜索二叉树&#xff08;AVL&#xff09;》&#xff0c;感谢大家对我上一篇的支持&#xff0c;如有什么问题&#xff0c;还请多多指教 &#xff01; 文章目录 :maple_leaf:AVL树:maple_leaf:…

Redis 7.x 系列【27】集群原理之通信机制

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Redis 版本 7.2.5 源码地址&#xff1a;https://gitee.com/pearl-organization/study-redis-demo 文章目录 1. 概述2 节点和节点2.1 集群拓扑2.2 集群总线协议2.3 流言协议2.4 心跳机制2.5 节点握…

matlab仿真 数字信号载波传输(下)

&#xff08;内容源自详解MATLAB&#xff0f;SIMULINK 通信系统建模与仿真 刘学勇编著第七 章内容&#xff0c;有兴趣的读者请阅读原书&#xff09; clear all M8; msg[1 4 3 0 7 5 2 6]; ts0.01; T1; %t0:ts:T; t0:ts:T-ts; %x0:ts:length(msg); x0:ts:length(msg)-ts; f…

爬虫 APP 逆向 ---> 粉笔考研

环境&#xff1a; 粉笔考研 v6.3.15&#xff1a;https://www.wandoujia.com/apps/1220941/history_v6031500雷电9 模拟器&#xff1a;https://www.ldmnq.com/安装 magisk&#xff1a;https://blog.csdn.net/Ruaki/article/details/135580772安装 Dia 插件 (作用&#xff1a;禁…

前端开发知识-vue

大括号里边放键值对&#xff0c;即是一个对象。 一、vue可以简化前端javascript的操作。 主要特点是可以实现视图、数据的双向绑定。 使用vue主要分为三个步骤&#xff1a; 1.javascript中引入vue.js 可以src中可以是vue的网址&#xff0c;也可以是本地下载。 2.在javasc…

地形材质制作(能使地面湿润)

如图&#xff0c;创建一个材质并写以下逻辑 Landscape Layer Blend节点能使在地形模式绘制中有三个选择&#xff0c;根据以上逻辑&#xff0c;Red是原材质,Green是绿色材质也就是草&#xff0c;Blue为水&#xff08;这个我认为比较重要&#xff09; Blue的颜色最好为这个 这个节…

董宇辉离职,我一点都不意外!只不过感觉来的太快

下面这张图&#xff0c;是我在半年多前写的一段随笔&#xff0c;没想到来的这么快&#xff01; 碰巧的是今天中午&#xff0c;在开发者群里有两位老铁自曝&#xff0c;本以为能公司干到老&#xff0c;但公司却不给机会&#xff0c;已经不在是公司员工了。 最近&#xff0c;晓衡…

一些关于颜色的网站

欢迎来到 破晓的历程的 博客 ⛺️不负时光&#xff0c;不负己✈️ 1、中国传统色 2、网页颜色选择器 3、渐变色网站 4、多风味色卡生成 5、波浪生成 6、半透明磨砂框 色卡组合

苍穹外卖01

0. 配置maven (仅一次的操作 1.项目导入idea 2. 保证nginx服务器运行 &#xff08;nginx.exe要在非中文的目录下&#xff09; 开启服务&#xff1a; start nginx 查看任务进程是否存在&#xff1a; tasklist /fi "imagename eq nginx.exe" 关闭ngi…

SAPUI5基础知识20 - 对话框和碎片(Dialogs and Fragments)

1. 背景 在 SAPUI5 中&#xff0c;Fragments 是一种轻量级的 UI 组件&#xff0c;类似于视图&#xff08;Views&#xff09;&#xff0c;但它们没有自己的控制器&#xff08;Controller&#xff09;。Fragments 通常用于定义可以在多个视图中重用的 UI 片段&#xff0c;从而提…

【数据结构--排序】

目录 一、排序概述1.1、排序的相关定义1.2、排序用到的结构与函数 二、常见排序算法2.1、冒泡算法&#xff08;交换顺序&#xff09;&#xff08;1&#xff09;算法&#xff08;2&#xff09;性能分析 2.2、简单选择排序&#xff08;1&#xff09;算法&#xff08;2&#xff09…

express连接mysql

一、 安装express npm install express --save二、express配置 //引入 const express require("express"); //创建实例 const app express(); //启动服务 app.listen(8081, () > {console.log("http://localhost:8081"); });三、安装mysql npm i m…

《昇思25天学习打卡营第6天|ResNet50图像分类》

写在前面 从本次开始&#xff0c;接触一些上层应用。 本次通过经典的模型&#xff0c;开始本次任务。这里开始学习resnet50网络模型&#xff0c;应该也会有resnet18&#xff0c;估计18的模型速度会更快一些。 resnet 通过对论文的结论进行展示&#xff0c;说明了模型的功能&…

第2章 编译SDK

安装编译依赖 sudo apt-get update sudo apt-get install clang-format astyle libncurses5-dev build-essential python-configparser sconssudo apt-get install repo git ssh make gcc libssl-dev liblz4-tool \ expect g patchelf chrpath gawk texinfo chrpath diffstat …

springboot促进高等教育可持续发展管理平台-计算机毕业设计源码36141

摘 要 随着全球对可持续发展的日益关注&#xff0c;高等教育作为培养未来领导者和创新者的摇篮&#xff0c;其在推动可持续发展中的角色日益凸显。然而&#xff0c;传统的高等教育管理模式在应对复杂多变的可持续发展挑战时&#xff0c;显得力不从心。因此&#xff0c;构建一个…

stm32入门-----USART串口实现数据包的接收和发送

目录 前言 数据包 1.HEX数据包 2.文本数据包 C编程实现stm32收发数据包 1.HEX数据包的收发 2.文本数据包的收发 前言 前面几期讲解了USART串口发送数据和接收数据的原理&#xff0c;那本期在前面的基础上学习stm32 USART串口发送和接收数据包。本期包括两个项目&a…

数据库作业四

1. 修改 student 表中年龄&#xff08; sage &#xff09;字段属性&#xff0c;数据类型由 int 改变为 smallint &#xff1a; ALTER TABLE student MODIFY Sage SMALLINT; 2. 为 Course 表中 Cno 课程号字段设置索引&#xff0c;并查看索引&#xff1a; ALTER TABLE…

Linux系统下非root用户自行安装的命令切换为root权限时无法使用,提示comman not found解决办法

今天在开发的时候遇上了一个问题就是要去我们数据平台中进行数据的提取&#xff0c;数据存储用的是minio&#xff0c;一个MinIO部署由一组存储和计算资源组成&#xff0c;运行一个或多个 minio server 节点&#xff0c;共同作为单个对象存储库。独立的MinIO实例由具有单个 mini…

多区域DNS以及主从DNS的搭建

搭建多域dns服务器&#xff1a; 搭建DNS多区域功能&#xff08;Multi-Zone DNS&#xff09;主要是为了满足复杂网络环境下的多样化需求&#xff0c;提高DNS服务的灵活性、可扩展性和可靠性。 适应不同网络环境&#xff1a; 在大型组织、跨国公司或跨地域服务中&#xff0c;网…

微服务安全——SpringSecurity6详解

文章目录 说明SpringSecurity认证快速开始设置用户名密码基于application.yml方式基于Java Bean配置方式 设置加密方式自定义用户加载方式自定义登录页面前后端分离认证认证流程 SpringSecurity授权web授权:基于url的访问控制自定义授权失败异常处理方法授权:基于注解的访问控制…