赞
踩
传统的登录,也就是通过form表单登录,使用sequrity做安全框架,当一个用户点击登录,首先security会去找 userDetailsServese 类里边的 loadUserByUsername 方法 -> 参数传递用户名 -> 拿着用户名,去数据库中查找,如果用户名存在 -> 会返回 UserDetails,而UserDetails 是一个接口,对应实现类是User,User类中的参数有用户名、密码、权限,也就是说返回的有用户名、密码和权限,接着会对前端传过来的密码和数据库返回的密码进行比对。
内部通过 loadUserByUsername 接口 进行登录逻辑,
参数:就是登陆时传递的username账号
异常:用户名未找到
返回的类型为 UserDetails 接口
默认有三个实现了UserDetails 接口
平时开发主要用到: User类,注意与自定义的User不能混淆。
encode 方法,对前端传过来的密码进行加密;参数为前端传过来的密码。
matches 数据库的密码和前端传过来的密码进行匹配,参数一:前端传过来的密码,参数二:查询出来的密码;
默认有非常多的实现类。
常用的 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中的使用。
如果不自己实现,默认访问应用,会弹出一个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"));
- }
- }
继承
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),注意前边加/
- }
- }
解决:
请求授权,任何请求都需要授权,接着重启服务,访问任何页面,此时浏览器崩了:
- @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(); // 任何请求都需要授权
- }
- }
下边是我们写好的自定义登录页面:,点击登录会请求/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页面,完成一次从 登录自定义逻辑 -> 到我们自定义登录页面 -> 授权 -> 登陆成功 的请求。
上边登录成功大致流程跑通了,但是如果登录失败呢,需要添加一个失败的页面:
配置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(); // 任何请求都需要授权
- }
- }
账号密码输错了:
会转到失败页面:
上边我们自定义的登录页面,请求参数名是:username和password,请求方法是:post,这是security默认的,必须这么写,。
自定义参数名:
配置:
- 自定义成功处理器:
- 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配置:
下边接口在实际开发中用的很多。
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);
- }
- }
最后的权限是之前我们设置的:
先来看下之前登录失败的处理方式,通过 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"));
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。