Spring Security教程 第二弹 spring security核心源码分析

写在前面的话

更多Spring与微服务相关的教程请戳这里 Spring与微服务教程合集

1、Spring Security如何灵活集成多种认证技术?

首先是javax.security.auth.Subject类,而一个Subject类包含多个javax.security.Principal

Principal类源码:

public interface Principal {
    public boolean equals(Object another);
    public String toString();
    public int hashCode();
    public String getName();
    public default boolean implies(Subject subject) {
        if (subject == null)
            return false;
        return subject.getPrincipals().contains(this);
    }
}

spring security对principal做了一层封装,用Authentication表示

Authentication类源码:

public interface Authentication extends Principal, Serializable {
   //获取主体权限集合
    Collection<? extends GrantedAuthority> getAuthorities();

   //获取主体凭证,通常被称为用户密码
    Object getCredentials();

   //获取主体携带的详细信息
    Object getDetails();

   //获取主体,通常为用户名
    Object getPrincipal();

    //主体是否验证成功
    boolean isAuthenticated();

    void setAuthenticated(boolean var1) throws IllegalArgumentException;

}

由于大部分场景下身份验证都是基于用户名和密码进行的,所以Spring Security提供了一个UsernamePasswordAuthenticationToken类Authentication的子类)用于代指这一类证明

也可以用SSH KEY进行登录,但它不属于用户名和密码登录这个范畴,如有必要,也可以自定义实现

在使用表单登录中,每一个登录用户都被包装为一个 UsernamePasswordAuthenticationToken,从而在Spring Security的各个AuthenticationProvider中流动

AuthenticationProvider被Spring Security定义为一个验证过程

AuthenticationProvider接口的源码:

public interface AuthenticationProvider {
    //成功则返回一个验证完成的Authentication 
    Authentication authenticate(Authentication var1) throws AuthenticationException;

   //是否支持验证当前的Authentication类型
    boolean supports(Class<?> var1);

}

一次完整的认证可以包含多个AuthenticationProvider,一般由ProviderManager即AuthenticationManager的子类)管理

如下是ProviderManager的authenticate方法源码,当有多个AuthenticationProvider时,循环验证,只要有一个验证通过则通过

Iterator var8 = this.getProviders().iterator();

while(var8.hasNext())

2、如何自定义Provider?

Spring Security提供了多种常见的认证技术,包括但不限于以下几种:

  • HTTP层面的认证技术,包括HTTP基本认证和HTTP摘要认证两种
  • 基于LDAP的认证技术(Lightweight Directory Access Protocol,轻量目录访问协议)
  • 聚焦于证明用户身份的OpenID认证技术
  • 聚焦于授权的OAuth认证技术
  • 系统内维护的用户名和密码认证技术

其中,使用最为广泛的是由系统维护的用户名和密码认证技术,通常会涉及数据库访问。为了更好地按需定制,Spring Security 并没有直接糅合整个认证过程,而是提供了一个抽象的

AbstractUserDetailsAuthenticationProvider(是AuthenticationProvider的子类),在 AbstractUserDetailsAuthenticationProvider 中实现了基本的认证流程,通过继承AbstractUserDetailsAuthenticationProvider,

并实现retrieveUseradditionalAuthenticationChecks两个抽象方法即可自定义核心认证过程

DaoAuthenticationProvider就是继承了AbstractUserDetailsAuthenticationProvider

3、用户名密码是如何登陆的?

验证流程:

  1. 用户请求首先会进入到UsernamePasswordAuthenticationFilter中,此时的Authentication是未认证的
  2. 接着通过ProviderManager找到匹配的Provider,此处找到是DaoAuthenticationProvider,DaoAuthenticationProvider会根据UserDetailsService去查找UserDetails,接着执行校验逻辑
  3. 如果校验成功,则Authentication变成已认证的。最后沿着调用链返回时会经过SecurityContexPersistenceFilter,该过滤器在过滤器链的最前面,当请求过来的时候检查Session里是否有SecurityContex,有的话则拿出来放到线程里,如果没有则通过
     

UsernamePasswordAuthenticationFilter类的源码:

package org.springframework.security.web.authentication;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private String usernameParameter = "username";
    private String passwordParameter = "password";
    private boolean postOnly = true;

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    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 {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }
}

在该类中,可以看到如下信息:

  • 登陆接口是post请求
  • 登陆接口url为/login
  • 参数key分别为username和password

当然,这些信息我们可以通过自定义AbstractAuthenticationProcessingFilter 来个性化定制

4、自定义AuthenticationProvider与前后端分离登录实战

4.1、自定义AuthenticationProvider类

注意:自定义UserDetailsService见Spring Security教程 第一弹 初识spring security_初心JAVA-CSDN博客中的4.3节

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

@Component
public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if(userDetails.getPassword().equals(authentication.getCredentials())){
            System.out.println("密码校验成功");
        }else{
            System.out.println("密码校验错误");
            //必须抛出异常才能表示验证失败
            throw new BadCredentialsException("密码校验错误");
        }
    }

    @Override
    protected UserDetails retrieveUser(String s, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
        UserDetails userDetails = this.userDetailsService.loadUserByUsername(s);
        return userDetails;
    }
}

4.2、自定义登录成功处理类

import com.bobo.group.springsecuritybase.util.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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

@Component
public class JsonAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        ObjectMapper mapper = new ObjectMapper();
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        response.setStatus(200);
        PrintWriter writer = response.getWriter();
        String val = mapper.writeValueAsString(new Result<String>(200, "登录成功!", ""));
        writer.write(val);
        writer.flush();
        writer.close();
    }
}

4.3、自定义登录失败处理类

import com.bobo.group.springsecuritybase.util.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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

@Component
public class JsonAuthenticationFailureHandler implements AuthenticationFailureHandler {


    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        ObjectMapper mapper = new ObjectMapper();
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        response.setStatus(200);
        PrintWriter writer = response.getWriter();
        String val = mapper.writeValueAsString(new Result<String>(401, "登录失败!", ""));
        writer.write(val);
        writer.flush();
        writer.close();
    }
}

4.4、统一响应实体类

public class Result<T> {

    private Integer code;
    private String desc;
    private T data;
    public Result(Integer code, String desc, T data) {
        this.code = code;
        this.desc = desc;
        this.data = data;
    }
    public Integer getCode() {return code;}
    public void setCode(Integer code) {this.code = code;}
    public String getDesc() {return desc;}
    public void setDesc(String desc) {this.desc = desc;}
    public T getData() {return data;}
    public void setData(T data) {this.data = data;}
}

4.5、WebSecurityConfig类

import com.bobo.group.springsecuritybase.handler.CustomAuthenticationProvider;
import com.bobo.group.springsecuritybase.handler.JsonAuthenticationFailureHandler;
import com.bobo.group.springsecuritybase.handler.JsonAuthenticationSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //以下3个类用于自定义authenticationProvider
    @Autowired
    private CustomAuthenticationProvider customAuthenticationProvider;
    @Autowired
    private JsonAuthenticationSuccessHandler jsonAuthenticationSuccessHandler;
    @Autowired
    private JsonAuthenticationFailureHandler jsonAuthenticationFailureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //ant风格通配符。?匹配单个字符;*匹配0或任意个字符;**匹配0或任意多目录
                //需要认证+授权,且有admin(区分大小写)角色才能访问
                .antMatchers("/admin/**").hasRole("ADMIN")
                //需要认证+授权,且有user角色才能访问;spring security默认用户的角色就是user
                .antMatchers("/user/**").hasRole("USER")
                //公开资源,不需要认证+授权就能访问
                .antMatchers("/app/**","/**/*.html","/**/*.js").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable()
                .formLogin()
                .loginProcessingUrl("/auth/doLogin")
                .successHandler(jsonAuthenticationSuccessHandler)
                .failureHandler(jsonAuthenticationFailureHandler)
                .and()
                //当没有权限,响应码设为403,而不是默认的302.如果需要返回json数据也可以定义
                .exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint());
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定义authenticationProvider
        //其实也自定义了userDetailsService,因为在自定义的authenticationProvider类中,可以看到注入了userDetailsService
        auth.authenticationProvider(customAuthenticationProvider);
    }

}

4.6、前端登录页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="jquery-3.4.1.js"></script>
    <script>
        $(function () {
            $("#login").click(function () {
                var username=$("#username").val();
                var password=$("#password").val();
                $.ajax({
                    url: "http://localhost:8140/spring-security-base/auth/doLogin",
                    data:{
                        "username" : username,
                        "password" : password
                    },
                    method : "post",
                    success:function (result) {
                        alert(result.desc)
                    }
                });
            });
        });

    </script>
</head>
<body>
    <h1>Spring Security前后端分离登录认证测试</h1>
    <div>
        用户名:<input type="text" id="username"><br>
        密码:<input type="password" id="password"><br>
        <button id="login">登录</button>
    </div>

</body>
</html>

至此,前后端分离登录与自定义AuthenticationProvider就完成了。我们可以访问login.html,输入数据库中对应的用户名与密码进行登录认证。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

波波老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值