本节速记:
重点:写在数据库的配置
1.{noop}明文密码
2.{bcrypt},bcrypt类型加密密码,其他家秘密方案同理
3.二中说明Spring security中是允许多种加密方案共存的(shiro中也可以,但是要配置多个DBRealm)
4.注意的是不加密也算加密方案的一种,即不加密的也可以与加密的方案一起配置在数据库
一.项目配置.
与四中基本相同
二.密码加密
2.1.不适用代理的方法,即不在SecurityConfig里面加入配置passwordEncoder
这里我们就像shiro里面在测试类里面做示范
BCryptPasswordEncoder 是加密方法接口的实体类, 利用单向自适应函数进行密码加密的,最常见的加密方法(加密的强度变高,),输入的参数是强度,即计算机调用cpu的强度,在一段的时间内占用满你的cpu的资源, 让你没办法实现暴力破解密码
1.密码强度有固定范围即4-31(取值5-30)
2.加密密码自带盐加密底层原理方法>
关键二:密码加密的接口实现类(即加密方案MD5,SHI-512这种)关键二:
SpringSecurity的密码加密工具PasswordEncoderFactories
关键三:为什么默认方法是
BCryptPasswordEncoder,因为使用了代理模式
createDelegetingPasswordEncoder。
package com.huang.springsecurity;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.MessageDigestPasswordEncoder;
@SpringBootTest
class SpringSecurityApplicationTests {
/**
* SpringSecurity 中,密码加密自带盐
*/
@Test
void contextLoads() {
/*
利用单向自适应函数进行密码加密的,最常见的加密方法,输入的参数是强度,即计算机调用cpu的强度,在一段的时间内占用满你的cpu的资源,
让你没办法实现暴力破解密码
*/
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
for (int i = 0; i < 10; i++) {
System.out.println("encoder.encode(\"123\") = " + encoder.encode("123"));
}
//已经废弃了但是依旧是可以用的
MessageDigestPasswordEncoder md5 = new MessageDigestPasswordEncoder("MD5");
for (int i = 0; i < 3; i++) {
System.out.println("md5.encode(\"123\") = " + md5.encode("123"));
}
}
}
加密的结果
进行postman登录接口测试
发现不管是zhangsan用户还是lisi用户都没办法进行登录这是为什么?
没有加入前缀
重点:写在数据库的配置1.{noop}明文密码2.{bcrypt},bcrypt类型加密密码,其他家秘密方案同理3.二中说明Spring security中是允许多种加密方案共存的(shiro中也可以,但是要配置多个DBRealm)4.注意的是不加密也算加密方案的一种,即不加密的也可以与加密的方案一起配置在数据库前缀由来数据库写前缀的原因是用了代理方法createDelegetingPasswordEncoder
2.2 使用代理的方法,passwordEncoder中加入配置passwordEncoder
package com.huang.springsecurity.comments.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huang.springsecurity.comments.result.RespBean;
import com.huang.springsecurity.model.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.*;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder() {
//这个表示使用明文密码
// return NoOpPasswordEncoder.getInstance();
//表示使用 bcrypt 做密码加密
// return new BCryptPasswordEncoder();
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder(12));
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
/**
* 给登录页面放行
* Spring Security 给一个地址放行,有两种方式:
* 1. 被放行的资源,不需要经过 Spring Security 过滤器链(静态资源一般使用这种)。
* 2. 经过 Spring Security 过滤器链,但是不拦截(如果是一个接口想要匿名访问,一般使用这种)。
* <p>
* 下面这种方形方式是第一种
*
* @return
*/
@Bean
WebSecurityCustomizer securityCustomizer() {
return new WebSecurityCustomizer() {
@Override
public void customize(WebSecurity web) {
web.ignoring().antMatchers("/login.html");
}
};
}
/**
* 自己手动配置安全过滤器链
*
* @return
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//开始认证
http.authorizeRequests()
//请求路径如果是 /login.html,则这个请求可以匿名通过
.antMatchers("/login.html").anonymous()
//所有的请求,类似于 shiro 中的 /**
.anyRequest()
//必须要认证之后才能访问,类似于 shiro 中的 authc
.authenticated()
.and()
//开始配置登录表单
.formLogin()
//配置登录页面,如果访问了一个需要认证之后才能访问的页面,那么就会自动跳转到这个页面上来
.loginPage("/login.html")
//配置处理登录请求的接口,本质上其实就是配置过滤器的拦截规则,将来的登录请求就会在过滤器中被处理
.loginProcessingUrl("/doLogin")
//配置登录表单中用户名的 key
.usernameParameter("username")
//配置登录表单中用户密码
.passwordParameter("password")
//配置登录成功后的跳转地址
// .defaultSuccessUrl("/hello")
// .failureUrl("/login.html")
//登录成功处理器
//req:当前请求对象
//resp:当前响应对象
//auth:当前认证成功的用户信息
.successHandler((req, resp, auth) -> {
resp.setContentType("application/json;charset=utf-8");
User principal = (User) auth.getPrincipal();
principal.setPassword(null);
RespBean respBean = RespBean.ok("登录成功", principal);
String s = new ObjectMapper().writeValueAsString(respBean);
resp.getWriter().write(s);
})
//登录失败的回调
.failureHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
//登录失败可能会有多种原因
RespBean respBean = RespBean.error("登录失败");
if (e instanceof BadCredentialsException) {
respBean.setMsg("用户名或者密码输入错误,登录失败");
} else if (e instanceof UsernameNotFoundException) {
//默认情况下,这个分支是不会进来的,Spring Security 自动隐藏了了这个异常,如果系统中发生了 UsernameNotFoundException 会被自动转为 BadCredentialsException 异常然后抛出来
} else if (e instanceof LockedException) {
//如果 com.qfedu.security02.model.User.isAccountNonLocked 方法返回 false,就会进入到这里来
respBean.setMsg("账户被锁定,登录失败");
} else if (e instanceof AccountExpiredException) {
//com.qfedu.security02.model.User.isAccountNonExpired
respBean.setMsg("账户过期,登录失败");
} else if (e instanceof CredentialsExpiredException) {
respBean.setMsg("密码过期,登录失败");
} else if (e instanceof DisabledException) {
respBean.setMsg("账户被禁用,登录失败");
}
ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(respBean);
PrintWriter out = resp.getWriter();
out.write(s);
})
.and()
//关闭 csrf 防御机制,这个 disable 方法本质上就是从 Spring Security 的过滤器链上移除掉 csrf 过滤器
.csrf().disable()
.exceptionHandling()
//如果用户未登录就访问某一个页面,就会触发当前方法
.authenticationEntryPoint((req, resp, authException) -> {
resp.setContentType("application/json;charset=utf-8");
RespBean respBean = RespBean.error("尚未登录,请登录");
String s = new ObjectMapper().writeValueAsString(respBean);
resp.getWriter().write(s);
});
return http.build();
}
}
关键配置
@Bean
PasswordEncoder passwordEncoder() {
//这个表示使用明文密码
// return NoOpPasswordEncoder.getInstance();
//表示使用 bcrypt 做密码加密
// return new BCryptPasswordEncoder();
String encodingId = “bcrypt”;
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder(12));
encoders.put(“ldap”, new LdapShaPasswordEncoder());
encoders.put(“MD4”, new Md4PasswordEncoder());
encoders.put(“MD5”, new MessageDigestPasswordEncoder(“MD5”));
encoders.put(“noop”, NoOpPasswordEncoder.getInstance());
encoders.put(“pbkdf2”, new Pbkdf2PasswordEncoder());
encoders.put(“scrypt”, new SCryptPasswordEncoder());
encoders.put(“SHA-1”, new MessageDigestPasswordEncoder(“SHA-1”));
encoders.put(“SHA-256”, new MessageDigestPasswordEncoder(“SHA-256”));
encoders.put(“sha256”, new StandardPasswordEncoder());
encoders.put(“argon2”, new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
三.密码的升级
3.1 在SecurityConfig里面加入配置passwordEncoder的基础上userservice继承方法
userService
package com.qfedu.security02.service;
import com.qfedu.security02.mapper.UserMapper;
import com.qfedu.security02.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserService implements UserDetailsService, UserDetailsPasswordService {
@Autowired
UserMapper userMapper;
/**
* 根据用户名查询用户对象
* @param username 用户登录时候输入的用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User u = userMapper.loadUserByUsername(username);
if (u == null) {
//说明用户名不存在
throw new UsernameNotFoundException("账户不存在");
}
return u;
}
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
User u = (User) user;
u.setPassword(newPassword);
userMapper.updatePassword(u);
return u;
}
}
注意:
1.先加入代理方法
2.继承类UserDetailsPasswordService
3.覆写更新密码的方法updatePassword
4.这样以后每次用户重新登录的时候底层会自动识别他的加密方法和加密强度,并且去给他升级成为新的加密方法和加密强度