SpringSecurity默认的登录逻辑如下:
1、认证 -- 也就是登录的大致流程:
传统的登录,也就是通过form表单登录,使用sequrity做安全框架,当一个用户点击登录,首先security会去找 userDetailsServese 类里边的 loadUserByUsername 方法 -> 参数传递用户名 -> 拿着用户名,去数据库中查找,如果用户名存在 -> 会返回 UserDetails,而UserDetails 是一个接口,对应实现类是User,User类中的参数有用户名、密码、权限,也就是说返回的有用户名、密码和权限,接着会对前端传过来的密码和数据库返回的密码进行比对。
登录逻辑
内部通过 loadUserByUsername 接口 进行登录逻辑,
参数:就是登陆时传递的username账号
异常:用户名未找到
返回类型
返回的类型为 UserDetails 接口
UserDetails 接口的三个实现类
默认有三个实现了UserDetails 接口
User类
平时开发主要用到: User类,注意与自定义的User不能混淆。
密码处理 - PasswordEncoder接口
encode 方法,对前端传过来的密码进行加密;参数为前端传过来的密码。
matches 数据库的密码和前端传过来的密码进行匹配,参数一:前端传过来的密码,参数二:查询出来的密码;
PasswordEncoder接口的实现类
默认有非常多的实现类。
常用的 BCryptPasswordEncoder 实现类,也是官方推荐的一个。
有三个常量
private final int strength; //密码的加密强度 默认为10
private final BCryptVersion version;
private final SecureRandom random;
对应encode方法
大致流程
首先判断前端传过来的密码是否为空,是的话直接抛异常。有密码,接着生成盐,判断是否有随机数,没有的话使用版本号、加密强度生成盐;有的话,使用 版本号、加密强度、随机数生成盐,最后,根据哈希算法,对密码+盐进行加密,通过 strength 加密强度进行加密,加密强度默认为10。
测试
搞懂上边的大致流程,我们来用一个密码简单测试下,看下生成的密码和密码比对
@Test
void contextLoads() {
BCryptPasswordEncoder bp = new BCryptPasswordEncoder();
String psw = bp.encode("123");
System.out.println("加密后的密码:"+ psw);
}
下边就是对前端过传来的密码进行加密之后的样子:
对密码"123"多次执行,生成出来的加密密码也是不一样的,因为内部生成的盐每一次都是随机生成!
查询出来的密码如何与上边加密后的密码进行比对呢, 可以使用 matches() 方法:
=======================================================
言归正传,记录下在springboot中的使用。
1、简单实现自己的登陆逻辑
如果不自己实现,默认访问应用,会弹出一个security默认的一个登录页面,默认的账号和密码是security提供的,账号admin,密码是每次启动应用随机生成的,下边写一个自己的登录逻辑:
因为要模拟密码加密,所以加一个PasswordEncoder的 Bean,下边创建了一个Security的配置类:
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 {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
实现自己的登陆逻辑主要是要实现UserDetailsService接口:
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 UserDetailServiceImpl implements UserDetailsService {
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println(username);
// 账号admin,密码:123
// 仿数据库
if(!username.equals("admin")) {
throw new UsernameNotFoundException("用户名不存在");
}
// 比较密码,这里仿一个密码加密
String password = passwordEncoder.encode("123");
// 账号、密码、权限
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
}
}
2、实现自己的登录页面,默认security自带的登录页,实现自己的如下:
继承
WebSecurityConfigurerAdapter ,重写 configure方法
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ··· ···
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单的登录
http.formLogin()
.loginPage("/login.html"); // 自己的登录页面(在static中的html),注意前边加/
}
}
3、上边我们自定义了:登录页面和登陆逻辑,此时,在浏览器访问任何api接口或应用都能访问,显然不是我们想要的,因为没有相应的权限了,没有登录,直接就能访问任何页面???
解决:
请求授权,任何请求都需要授权,接着重启服务,访问任何页面,此时浏览器崩了:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ··· ···
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单的登录
http.formLogin()
.loginPage("/login.html"); // 登录页面
// 请求授权
http.authorizeRequests()
.anyRequest().authenticated(); // 任何请求都需要授权
}
}
原因:任何请求都需要授权,它会重定向到登录页面,但是登录页面也需要授权,所以死循环。
解决:添加一组放行的路径,规定登录页面不需要授权。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单的登录
http.formLogin()
.loginPage("/login.html"); // 登录页面
// 请求授权
http.authorizeRequests()
// 匹配一个可以放行的路径
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated(); // 任何请求都需要授权
}
}
4、到了我们自定义的登录页面之后,我们想要登录后直接访问之前写好的登陆成功页面:
下边是我们写好的自定义登录页面:,点击登录会请求/loigin接口,
login接口会被下边loginProcessingUrl() 方法匹配到,然后会跳转至 /toSuccess api,这个api必须是post请求的,因为successForwardUlr() 方法只接收post请求,而且该方法的参数必须是一个控制器中定义的api接口,且控制器中返回的是一个视图名,原因在最后源码中有说明,内部只是一个简单个跳转!!!
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单的登录
http.formLogin()
.loginPage("/login.html") // 登录页面
// 登录要处理的url
.loginProcessingUrl("/login") // 这个路径需要跟表单提交的登录路径相同,才能走我们自定义的登陆逻辑
// 登陆成功后跳转的页面(只接受post请求)
.successForwardUrl("/toSuccess");
// 关闭防火墙(关闭csrf 跨站请求伪造)
http.csrf().disable();
// 请求授权
http.authorizeRequests()
// 匹配一个可以放行的路径
.mvcMatchers("/login.html").permitAll()
.anyRequest().authenticated(); // 任何请求都需要授权
}
}
到控制器下的toSuccess接口,里边会重定向到success.html页面,完成一次从 登录自定义逻辑 -> 到我们自定义登录页面 -> 授权 -> 登陆成功 的请求。
5、登录失败失败的处理。
上边登录成功大致流程跑通了,但是如果登录失败呢,需要添加一个失败的页面:
配置ssecurity:
(1)failureForwardUrl() 失败的跳转的api。
(2)mvcMatchers("error.html") 需要添加要放行的页面,否则到不了失败页面!
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单的登录
http.formLogin()
.loginPage("/login.html") // 登录页面
// 登录要处理的url
.loginProcessingUrl("/login") // 这个路径需要跟表单提交的登录路径相同,才能走我们自定义的登陆逻辑
// 登陆成功后跳转的页面(只接受post请求)
.successForwardUrl("/toSuccess")
// 登陆失败后跳转的页面(只接受post请求)
.failureForwardUrl("/toError");
// 关闭防火墙(关闭csrf 跨站请求伪造)
http.csrf().disable();
// 请求授权
http.authorizeRequests()
// 匹配一个可以放行的路径
.mvcMatchers("/login.html", "/error.html").permitAll()
.anyRequest().authenticated(); // 任何请求都需要授权
}
}
账号密码输错了:
会转到失败页面:
6、自定义的请求参数名
上边我们自定义的登录页面,请求参数名是:username和password,请求方法是:post,这是security默认的,必须这么写,。
自定义参数名:
配置:
7、自定义页面跳转
自定义成功处理器:
successHandler(new PageSuccessHandler("http://www.baidu.com"))
登录成功之后的页面跳转,上边是通过一个api接口,转到控制层,在由控制器里边重定向到一个html页面,但是,现在前后端分离项目,如果想直接跳转到百度页面,该如何做?
successForwardUrl("http://www.baidu.com"),此时单纯这样写无法完成此需求!
看下successForwardUrl源码:
解决:
实现我们自定义的跳转,其实也很简单,实现 AuthenticationSuccessHandler 接口,重写:onAuthenticationSuccess方法即可。
import org.springframework.security.core.Authentication;
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 PageSuccessHandler implements AuthenticationSuccessHandler {
private final String url;
public PageSuccessHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
// 重定向到url地址
httpServletResponse.sendRedirect(this.url);
}
}
修改security配置:
8、来看下Authentication
下边接口在实际开发中用的很多。
onAuthenticationSuccess 方法的第三个参数 :Authentication
public class PageSuccessHandler implements AuthenticationSuccessHandler {
private final String url;
public PageSuccessHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
// 获取当前登录的这个用户
User user = (User) authentication.getPrincipal();
System.out.println(user.getUsername()); // 获取登录之后的用户账号
System.out.println(user.getPassword()); // 获取登陆之后的密码,为了安全security默认为null
System.out.println(user.getAuthorities()); // 获取当前用户的权限
httpServletResponse.sendRedirect(this.url);
}
}
最后的权限是之前我们设置的:
9、定义登录失败的处理器
先来看下之前登录失败的处理方式,通过 failureForwardUrl(url)这个url必须对应的是控制器中的返回的视图,如果直接写一个xxx.html是不可行的!!!源码跟登陆成功的处理方式一样,只不过最后 onAuthenticationFailure() 授权失败的处理方法参数不一样:
实现自定义登录失败的处理(跟成功的类似):
实现 AuthenticationFailureHandler 接口,重写 onAuthenticationFailure() 方法
package com.lxc.sequrity.config;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class PageFailHandler implements AuthenticationFailureHandler {
private final String url;
public PageFailHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
httpServletResponse.sendRedirect(this.url);
}
}
最后修改配置:
登陆失败,跳转error.html成功
总结:
不管登录成功的处理逻辑或登录失败的处理逻辑,其实都是需要实现成功认证的处理接口和失败的认证处理接口,重写里边的方法,在方法中定义我们的登录逻辑。其次,在配置类中 调用 :
successHandler(new 自定义的处理类) 或 failureHandler(new 自定义的处理类) 处理方法
补充:
BCryptPasswordEncoder 和、passwordEncoder两个密码类,security内部使用的,对密码进行加密,对比等。
@Configuration
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder () {
return new BCryptPasswordEncoder();
}
}
(2)我们自己实现登录逻辑,所以,必须要实现 userDetailsServese () 接口
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("username:"+username);
// 【第一步】
// 参数:username:前端传过来的账号,拿username直接去数据库查询,如果username为null 直接抛出异常 :UsernameNotFoundException
// 为了方便这里直接,写死
String databaseUsername = "admin";
if(databaseUsername.equals(username)) {
throw new UsernameNotFoundException("用户名不存在");
}
// 【第二步】
// 密码比对
// 我们在这拿到前端传过来的密码,springSequrity 已经帮我们加密了,此时,在这里我们只是把加密后的
// 密码和数据库查询到的密码做比对即可。
// 这里模拟加密,实际开发中,注册时就已经加密了,应该使用matchs()进行比对,成功返回 UserDetails
// 下边应该写 matchers() 方法,进行密码比对,为了方便这里跳过密码比对,直接模
// 拟一个加密后的密码
String password = passwordEncoder.encode("123");
return new User(username,
password,
// AuthorityUtils 权限的工具类
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
}
}