SpringSecurity 认证详解

Posted 流楚丶格念

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity 认证详解相关的知识,希望对你有一定的参考价值。

文章目录

自定义认证逻辑案例

相关API介绍

UserDetailsService 自定义逻辑

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的(如下图密码)。

但是在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。

接口定义如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.security.core.userdetails;

public interface UserDetailsService 
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;


该接口抽象方法的返回值 UserDetails 是一个接口,定义如下:

package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;

public interface UserDetails extends Serializable 
	// 获取所有权限
    Collection<? extends GrantedAuthority> getAuthorities();
	// 获取密码
    String getPassword();
	// 获取用户名
    String getUsername();
	// 是否账号过期
    boolean isAccountNonExpired();
	// 是否账号被锁定
    boolean isAccountNonLocked();
	// 凭证(密码)是否过期
    boolean isCredentialsNonExpired();
	// 是否可用
    boolean isEnabled();

要想返回 UserDetails 的实例就只能返回接口的实现类。

SpringSecurity 中提供了如下的实例。对于我们只需要使用里面的 User 类即可。
注意 User 的全限定路径是:org.springframework.security.core.userdetails.User 此处经常和系统中自己开发的 User 类弄混。

在 User 类中提供了很多方法和属性:

构造方法参数说明:

参数含义说明
username用户名用户名应该是客户端传递过来的用户名
password密码密码应该是从数据库中查询出来的密码,Spring Security 会根据 User 中的 password 和客户端传递过来的 password 进行比较。如果相同则表示认证通过,如果不相同表示认证失败。
authorities用户具有的权限。此处不允许为 nullauthorities 里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限,如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 403 。
通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”) 来创建 authorities 集合对象的。参数是一个字符串,多个权限使用逗号分隔。

注意: username 是客户端表单传递过来的数据。默认情况下参数名必须 username,否则无法接收

PasswordEncoder 密码编码器接口

Spring Security 要求容器中必须有 PasswordEncoder 实例

PasswordEncoder 实例源码如下:

package org.springframework.security.crypto.password;

public interface PasswordEncoder 
    String encode(CharSequence rawPassword);

    boolean matches(CharSequence rawPassword, String encodedPassword);

    default boolean upgradeEncoding(String encodedPassword) 
        return false;
    

当我们自定义登录逻辑时要求必须给容器注入 PaswordEncoder 的 bean 对象。

接口函数介绍:

  • encode():把参数按照特定的解析规则进行解析。

  • matches():验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。

    • 如果密码匹配,则返回 true;

    • 如果不匹配,则返回 false。

    第一个参数表示需要被解析的密码。第二个参数表示存储的密码。

  • upgradeEncoding():如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回 false。默认返回 false。

PaswordEncoder 实现类有很多:

比较常用的BCryptPasswordEncoder

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

测试代码如下:

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

public class MyTest 
    @Test
    public void test()
        //创建解析器
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        //对密码进行加密
        String password = encoder.encode("123456");
        System.out.println("------------"+password);
        //判断原字符加密后和内容是否匹配
        boolean result1 = encoder.matches("123456",password);
        boolean result2 = encoder.matches("456789",password);

        System.out.println("匹配结果 result1 :"+result1);
        System.out.println("匹配结果 result2 :"+result2);
    

运行结果:

代码实现

编写配置类

package com.yyl.springsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig 

    /**
     * 将 PasswordEncoder 注入容器
     * @return
     */
    @Bean
    public PasswordEncoder getPwdEncoder()
        return new BCryptPasswordEncoder();
    

编写认证服务实现类

package com.yyl.springsecurity.service.Impl;

import org.springframework.beans.factory.annotation.Autowired;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService 

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
        // 查询数据库判断用户名是否存在,如果不存在抛出相应异常
        if (!username.equals("admin")) 
            throw new UsernameNotFoundException("用户名不存在");
        
        // 把查询出来的密码进行解析,或直接把 password 放到构造方法中。
        // 理解:password 就是数据库中查询出来的密码,查询出来的内容不是 123
        String password = passwordEncoder.encode("123");
        return new User(username, password,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    


代码测试

重启项目后,在浏览器中输入账号:admin,密码:123后,看看可不可以正确进入到 index.html 页面。


没问题

自定义页面案例

自定义登录页面

虽然 Spring Security 给我们提供了登录页面,但是对于实际项目中,大多喜欢使用自己的登录页面。所以 Spring Security 中不仅仅提供了登录页面,还支持用户自定义登录页面。实现过程也比较简单,只需要修改配置类即可。

编写登录页面

在static目录下创建login.html登录页面,在页面中<form>的 action 不编写对应控制器也可以。

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

修改配置类

写配置类中主要是设置哪个页面是登录页面。

配置类需要继承WebSecurityConfigurerAdapter,并重写configure 方法。

successForwardUrl()登录成功后跳转地址 loginPage() 登录页面 loginProcessingUrl 登录页面表单提交地址,此地址可以不真实存在。

配置 antMatchers() 匹配内容permitAll() 允许不要登录就能访问

package com.yyl.springsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter 

    /**
     * 将 PasswordEncoder 注入容器
     * @return
     */
    @Bean
    public PasswordEncoder getPwdEncoder()
        return new BCryptPasswordEncoder();
    

    /**
     * 重写configure方法
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception 
        // 表单认证
        http.formLogin()
                .loginProcessingUrl("/login")   // 处理登录的url,需要执行 UserDetailsServiceImpl
                .successForwardUrl("/toMain")   // 登录成功之后跳转
                .loginPage("/login.html");      // 登录页面
        // url 拦截
        http.authorizeRequests()
                .antMatchers("/login.html") //匹配的url 不要登录就能访问
                .permitAll()    //允许上述的antMatchers 匹配的url 直接访问,不要登录
                .anyRequest()   //任何请求
                .authenticated(); //其他anyRequest所有的请求都必须被认证。必须登录后才能访问。
        http.csrf().disable();  //关闭csrf
    


编写控制器

@Controller
public class LoginController 
    @RequestMapping("toMain")
    public String toMain()
        return "redirect:/main.html";
    

重启程序测试


没有问题

认证过程其他常用配置

失败跳转

表单处理中成功会跳转到一个地址,失败也可以跳转到一个地址中。

实现步骤如下:

在 static 目录下编写失败页面,fail.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>fail.html</h1>
    <h1>自定义fail页面</h1>
</body>
</html>

修改表单配置在配置方法中表单认证部分添加failureForwardUrl()方法,表示登录失败跳转的url。此处依然是POST 请求,所以跳转到可以接收 POST请求的控制器/fail 中。

http.formLogin()
.loginProcessingUrl("/login") //当发现/login 时认为是登录,需要执行 UserDetailsServiceImpl
.successForwardUrl("/toMain") //此处是 post 请求
.failureForwardUrl("/fail") //登录失败跳转地址
.loginPage("/login.html");

添加控制器方法

@PostMapping("/fail") 
public String fail() 
	return "redirect:/fail.html";

设置fail.html 不需要认证

// url 拦截
http.authorizeRequests()
        .antMatchers("/login.html","/fail.html") //匹配的url 不要登录就能访问

重启程序测试一下,登录个错误的


然后就跳转到自定义失败页面

自定义请求中的用户名和密码

当进行登录时会执行 UsernamePasswordAuthenticationFilter 过滤器,该过滤器类定义如下:

通过查看源码知道,参数名默认必须为username和password,并且只接收post请求。那如何更改默认的参数名呢?

修改配置对象

// 表单认证
http.formLogin()
        .loginProcessingUrl("/login")   // 处理登录的url,需要执行 UserDetailsServiceImpl
        .successForwardUrl("/toMain")   // 登录成功之后跳转
        .failureForwardUrl("/fail")
        .loginPage("/login.html")     // 登录页面
        .usernameParameter("myusername")
        .passwordParameter("mypassword");


配置好后,页面就可以根据配置进行自定义参数名了,我们在login.html页面进行设置我们传入后端的参数


重启程序进行测试没有问题

自定义登录成功处理器

我们可以实现自定义一个可以重定向的登录成功处理器,我们先查看successForwardUrl()源码

内部是通过 successHandler()方法进行控制成功后交给哪个类进行处理。


ForwardAuthenticationSuccessHandler 内部就是最简单的请求转
发。由于是请求转发,当遇到需要跳转到站外或在前后端分离的项目中就无法使用了。

通过追踪上面源码,我们可以发现只要我们自己再编写一个控制器类实现 AuthenticationSuccessHandler 接口,然后再配置应用我们自己编写的控制器类就可以了

代码如下:

package com.yyl.springsecurity.handler;

import com.alibaba.fastjson.JSONObject;
import com.yyl.springsecurity.common.JsonResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

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

/**
 * 自定义的认证成功后处理器
 */
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler 
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication)
            throws IOException, ServletException 
        //通过Authentication对象获取User信息
        User user = (User) authentication.getPrincipal();
        System.out.println(user.getUsername());
        System.out.println(user.getPassword());
        System.out.println(user.getAuthorities());

        //返回json
        JsonResult jsonResult = new JsonResult(0, "登录成功", null);
        String json = JSONObject.toJSONString(jsonResult);
        //设置返回值类型和编码
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        //json输出到前端
        response.getWriter().write(json);
    


修改配置类使用 successHandler()方法设置成功后交给哪个对象进行处理

 // 表单认证
 http.formLogin()
         .loginProcessingUrl("/login")   // 处理登录的url,需要执行 UserDetailsServiceImpl
         // .successForwardUrl("/toMain")   // 登录成功之后跳转
         .successHandler(new MyAuthenticationSuccessHandler())
         .failureForwardUrl("/fail")
         .loginPage("/login.html")     // 登录页面
         .usernameParameter("myusername")
         .passwordParameter("mypassword");

自定义登录失败处理器

同理:我们也自定义一个可以重定向的登录失败处理器,我们先查看failureForwardUrl()源码

内部调用的是 failureHandler()方法


ForwardAuthenticationFailureHandler 中 也 是 一 个 请 求 转 发 , 并 在 request 作用域中设置 SPRING_SECURITY_LAST_EXCEPTION 的 key,内容为异常对象。

编写一个控制器类实现 AuthenticationFailureHandler 接口

SpringSecurity框架详解

SpringSecurity基础功能详解

聊聊spring security oauth2的几个endpoint的认证

Spring Security---Oauth2详解

Shiro 登录认证源码详解

springsecurity如何保存重定向地址