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 | 用户具有的权限。此处不允许为 null | authorities 里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限,如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 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框架详解