mermaid
sequenceDiagram
participant 客户端
participant C AS 自定义Controller登录接口
participant P AS ProviderManager
participant D AS DaoAuthenticationProvider
participant U AS 自定义UserDetailsService
participant 数据库
客户端->>C: 提交用户名和密码
C->>P:
P->>D:
D->>U:
U->>数据库:
数据库->>U:
Note over U: 封装自定义的UserDetails对象
U->>D: 返回UserDetails对象
D->>P:
P->>C:
Note over C: 认证通过,生成jwt
Note over C: 并存入信息到redis,方便后续校验
C->>客户端: 返回 jwtUserDetailsService
java
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}java
// 提供了更多的关于用户的方法
public interface UserDetailsManager extends UserDetailsService {
// 创建新用户
void createUser(UserDetails user);
// 更新已有用户的信息
void updateUser(UserDetails user);
// 删除用户
void deleteUser(String username);
// 更改用户的密码
void changePassword(String oldPassword, String newPassword);
// 确认用户是否存在
boolean userExists(String username);
}自定义 UserDetailsManager 的实现类
参照 UserDetailsManager 接口的实现类 InMemoryUserDetailsManager 来写
java
// InMemoryUserDetailsManager
public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = (UserDetails)this.users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
} else {
return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}
}
}java
@Service
public class UserDetailsServiceImpl implements UserDetailsManager, UserDetailsPasswordService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询出用户信息
LoginUser loginUser = userMapper.selectByUserName(username);
if (loginUser == null) {
throw new UsernameNotFoundException(username + "用户不存在");
} else {
// 这个User是SpringSecurity的User
return new User(loginUser.getUsername(), loginUser.getPassword(), loginUser.isEnabled(), loginUser.isAccountNonExpired(), loginUser.isCredentialsNonExpired(), loginUser.isAccountNonLocked(), loginUser.getAuthorities());
}
}
}自定义 UserDetails 的实现类
java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private Integer userId;
private String userName;
private String userPassword;
private String userAuthority;
// 这个方法返回一个权限集合,表示用户具有的角色。Spring Security中的角色通常以ROLE_开头
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
return authorities;
}
// 返回用户的密码
@Override
public String getPassword() { return userPassword; }
// 返回用户的用户名
@Override
public String getUsername() { return userName; }
// 账户是否过期,如果返回false,那么账号就是过期的
@Override
public boolean isAccountNonExpired() { return true; }
// 账户是否锁定,如果返回false,那么账号就是被锁定的
@Override
public boolean isAccountNonLocked() { return true; }
// 凭证是否过期,如果返回false,那么凭证就是过期的
@Override
public boolean isCredentialsNonExpired() { return true; }
// 账户是否可用,如果返回false,那么账号就是不可用的
@Override
public boolean isEnabled() { return true; }
}密码加密
[!quote] 密码加密的历史 从明文密码 -> 哈希算法【
可以很容易地被暴力破解】 -> 加盐密码【比哈希算法更好】 -> 自适应单向函数【使用“工作因子”占用 CPU 的资源,从而拖慢密码破解的速度】
[!hint]
BCryptPasswordEncoder
- 使用自适应单向函数
BCryptPasswordEncoder加密是不可逆的,而且就算明文一样,加密后也会不同【因为盐是随机的】- 工作因子越大,匹配时间也会越长,因为匹配的过程中也要进行加密
- 将创建好的
BCryptPasswordEncoder注入到 IOC 容器中,替换默认的PasswordEncoder,后续要使用直接依赖注入
java
@Configuration
public class SercurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}java
// 测试一下
@Test
public void testPasswordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 加密
String encode = bCryptPasswordEncoder.encode("123456");
System.out.println(encode); // $2a$10$OgvYJMWInoiOmGqBCdwrOub3glU9zb5Nm0O0xkcNeAjgMUsSELE5e
// 比较明文与密文是否匹配
boolean matches = bCryptPasswordEncoder.
matches("123456",
"$2a$10$OgvYJMWInoiOmGqBCdwrOub3glU9zb5Nm0O0xkcNeAjgMUsSELE5e");
System.out.println(matches);
}
---
$2a$10$n/zIwDJII2Mzzo6XTWnn0uuWmHZ6Q44R.nl7DlkgfSieS.JkRVOzS
true认证成功/失败 / 未认证

以下流程的前提是前后端分离开发,后端只需要返回 JSON 数据给前端,前端根据后端返回的结果来处理页面的跳转
成功
- 自定义 MyAuthenticationSuccessHandler 实现 AuthenticationSuccessHandler 接口,用于登录成功后,做一些处理
java
package com.example.spring_security.infrastructure.config;
// 自定义登录成功处理器,在登录成功后,做一些处理
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
// 登录成功后,做一些处理
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Object credentials = authentication.getCredentials();
Object details = authentication.getDetails();
Object principal = authentication.getPrincipal();
Object authorities = authentication.getAuthorities();
boolean authenticated = authentication.isAuthenticated();
Result<String> responseResult = Result.buildResult(Result.Status.OK, "登录成功",
"密码:" + credentials +
",详情:" + details +
",主体:" + principal +
",权限:" + authorities +
",是否认证:" + authenticated);
// 定义json数据
ObjectMapper objectMapper = new ObjectMapper();
String jsonData = objectMapper.writeValueAsString(responseResult);
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(jsonData);
}
}- 在 Security 配置文件
SecurityConfig中配置 successHandler
java
http
……
.formLogin(form -> {
form.loginPage("http://localhost:5173/")
.loginProcessingUrl("/login")
.successHandler(new MyAuthenticationSuccessHandler());
});失败
- 自定义 MyAuthenticationFailureHandler 实现 AuthenticationFailureHandler 接口,用于登录失败后,做一些处理
java
package com.example.spring_security.infrastructure.config;
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
Logger logger = LoggerFactory.getLogger(MyAuthenticationFailureHandler.class);
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
logger.atInfo().log("登录失败,我要做一些处理");
String localizedMessage = exception.getLocalizedMessage();
Result<String> responseResult = Result.buildResult(Result.Status.ERROR, "登录失败", localizedMessage);
ObjectMapper objectMapper = new ObjectMapper();
String jsonData = objectMapper.writeValueAsString(responseResult);
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(jsonData);
}
}- 在 Security 配置文件 SecurityConfig 中配置 failureHandler
java
.formLogin(form -> {
form.loginPage("http://localhost:5173/")
.loginProcessingUrl("/login")
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationFailureHandler());
});未认证
当用户还未认证,但是访问了一个需要认证之后才能访问的接口时,我们需要返回 JSON 数据给前端【默认是跳转到登录页面】
- 自定义 MyAuthenticationEntryPoint 实现 AuthenticationEntryPoint 接口
java
package com.example.spring_security.infrastructure.config;
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
Logger logger = LoggerFactory.getLogger(MyAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
logger.atInfo().log("未登录,我要做一些处理");
String localizedMessage = authException.getLocalizedMessage();
Result<String> responseResult = Result.buildResult(Result.Status.ERROR, "未登录", localizedMessage);
ObjectMapper objectMapper = new ObjectMapper();
String jsonData = objectMapper.writeValueAsString(responseResult);
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(jsonData);
}
}- 在 Security 配置文件
SecurityConfig中配置
java
// 异常处理
http.exceptionHandling(exceptionHandling -> {
exceptionHandling.authenticationEntryPoint(new MyAuthenticationEntryPoint());
});注销
- 自定义 MyLogoutSuccessHandler 实现 LogoutSuccessHandler 接口
java
package com.example.spring_security.infrastructure.config;
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
Logger logger = LoggerFactory.getLogger(MyLogoutSuccessHandler.class);
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.atInfo().log("注销成功了,我要做一些处理");
Result<Object> responseResult = Result.buildResult(Result.Status.OK, "注销成功");
ObjectMapper objectMapper = new ObjectMapper();
String jsonData = objectMapper.writeValueAsString(responseResult);
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(jsonData);
}
}- 在
SecurityConfig配置文件中配置
java
// 注销配置
http.logout(logout -> {
logout.logoutSuccessHandler(new MyLogoutSuccessHandler());
});功能
创建用户
- 定义 Controller 【
LoginUser实现了 UserDetails】
java
@RestController
public class UserController {
@Autowired
private UserService userService;
// 创建用户
@PostMapping("/create")
public Result createUser(@RequestBody LoginUser loginUser) {
userService.createUser(loginUser);
return Result.buildResult(Result.Status.OK, "创建用户成功");
}
}- 定义 UserService,UserServiceImpl
java
package com.example.spring_security.domain.service.impl;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDetailsServiceImpl userDetailsServiceImpl;
@Override
public void createUser(LoginUser loginUser) {
userDetailsServiceImpl.createUser(loginUser);
}
}- 定义 userDetailsServiceImpl
java
package com.example.spring_security.domain.service.impl;
import org.springframework.security.core.userdetails.User;
@Service
public class UserDetailsServiceImpl implements UserDetailsManager {
@Autowired
private UserMapper userMapper;
// 加载用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
LoginUser loginUser = userMapper.selectByUserName(username);
if (loginUser == null) {
throw new UsernameNotFoundException(username + "用户不存在");
} else {
return loginUser;
}
}
// 创建用户
@Override
public void createUser(UserDetails user) {
// 密码加密
PasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode(user.getPassword());
LoginUser loginUser = (LoginUser) user;
loginUser.setUserPassword(encode);
userMapper.createOneUser(loginUser);
}
……
}