在这块,我们使用Spring Security整合springboot,mybatis-plus以及restful等知识,从而来实现基于数据库的自定义用户认证过程。
代码实现思路
1. 准备一张用户表,必需字段有主键id以及账号密码,其他看自己需求进行添加。
2. 搭建好项目环境和配置好mybatis数据源。
3. 先写用户类,主要是通过实现UserDetails来完成,并且记得添加mybatis-plus相关注解进行配置
4. 同样按照mybatis-plus的规定来写mapper类以及service层的实现类
5. 然后写一个类来实现UserDetailsService从而重写里面的 loadUserByUsername方法,该方法是Security认证过程中核心方法之一,该方法的主要目的是通过获取的用户账号来对数据库用户表数据进行用户查询,即将认证的数据依赖到数据库内而不是依赖内存,而对于认证的过程则是依赖于另一个Security部分即AuthenticationManager,主要过程即,我们首先在配置类中对该类进行相关配置,大概就是将UserDetailsService中的loadUserByUsername结果即查询到的用户结果以及密码加密对象一并关联起来并且将他们给传入另一个管理类DaoAuthenticationProvider进行处理之后,因为AuthenticationManager是一个接口,不能直接返回该对象,因此我们需要借助它的实现类ProviderManager,即我们在处理完之后再将其传给ProviderManager之后直接返回是初始化对象即可,在此对于该认证类已经算是配置好了。
6. 接着就到控制类中写登录接口了,我们首先将配置好的AuthenticationManager对象直接注入,然后我们再借助UsernamePasswordAuthenticationToken来接收访问的登录数据,之后将其对象导入AuthenticationManager的Authenticate方法即可获得认证成功后Authentication对象(Authentication对象就是用来存储认证用户数据的),其实就是UserDetails对象,该对象就是认证成功后的用户对象,随后通过转型即可取出,最后对于接口的结果响应则不是很重要。
7. 最后再借助postman来测试下接口看是否存在bug即可。
准备数据表
搭建好项目环境
项目需要导的依赖
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- MVC访问起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- security起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 连接MySQL数据库依赖-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- mybatis-plus起步依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<!-- 测试依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder-jammy-base:latest</builder>
</image>
</configuration>
</plugin>
</plugins>
</build>
整个项目大致目录结构:
该项目需要用到的依赖:
通过框架快速搭建好项目之后进行数据源的配置(application.yml)
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/security?useUnicode=true&&characterEncoding=utf8&&useSSL=false&&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
结果类的设计
package com.boot.result;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
//统一响应结果
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private Integer code;//状态码 200-成功 403-认证失败 405-授权失败 400-操作失败
private String message;//提示信息
private T data;//响应数据
//快速返回操作成功响应结果(带响应数据)
public static <E> Result<E> success(E data) {
return new Result<>(200, "操作成功", data);
}
//快速返回操作成功响应结果
public static Result success() {
return new Result(200, "操作成功", null);
}
public static Result error(String message) {
return new Result(400, message, null);
}
}
实现用户类
因为是使用mybatis-plus,因此记得写上相关配置,数据层亦是
package com.boot.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@TableName("user")
public class User implements UserDetails {
@TableId(value = "id",type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String nickname;
//因为该项目主要是实现用户认证,因此在这对于权限相关先不管
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
数据层以及业务层
根据mybatis-plus规范来实现数据层以及业务层代码,在这涉及的都是mybatis-plus的知识
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
public interface UserService extends IService<User> {
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
配置自定义数据源以及认证过程
先写一个业务类来实现UserDetailsService从而重写里面的 loadUserByUsername方法,该方法是Security认证过程中核心方法之一,该方法的主要目的是通过获取的用户账号来对数据库用户表数据进行用户查询,即将认证的数据依赖到数据库内而不是依赖内存,而对于认证的过程则是依赖于另一个Security部分即AuthenticationManager,主要过程即,我们首先在配置类中对该类进行相关配置,大概就是将UserDetailsService中的loadUserByUsername结果即查询到的用户结果以及密码加密对象一并关联起来并且将他们给传入另一个管理类DaoAuthenticationProvider进行处理之后,因为AuthenticationManager是一个接口,不能直接返回该对象,因此我们需要借助它的实现类ProviderManager,即我们在处理完之后再将其传给ProviderManager之后直接返回是初始化对象即可,在此对于该认证类已经算是配置好了。
package com.boot.web;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.boot.entity.User;
import com.boot.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class SysUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/*
* @description: 自定义认证数据源
* @param: [username]
**/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",username);
User user = userMapper.selectOne(wrapper);
if(user==null) {
throw new UsernameNotFoundException("用户账号错误!!!");
}
return user;
}
}
在配置类写认证配置的同时也要对请求授权等进行配置
package com.boot.config;
import com.boot.web.SysUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.DefaultSecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private SysUserDetailsService userDetailsService;
/*
* @description: 总配置
* @date: 2024/4/30 0:25
* @param: [http]
**/
@Bean
public DefaultSecurityFilterChain securityWebFilterChain(HttpSecurity http) throws Exception {
//首先对请求访问进行授权配置
http.authorizeHttpRequests(auth-> auth
//对于/auth/下的所有路径均可访问
.requestMatchers("/auth/**").permitAll()
//任何访问请求均需要进行认证
.anyRequest().authenticated()
);
//关闭跨域漏洞防御
http.csrf(csrf->csrf.disable());
return http.build();
}
/*
* @description: 配置认证过程
* @date: 2024/4/30 0:26
* @return: org.springframework.security.authentication.AuthenticationManager
**/
@Bean
public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder) {
//因为AuthenticationManager是接口,因此借用其实现类来配置
// ProviderManager providerManager = new ProviderManager();
//使用该类来关联自定义认证数据源和密码加密编码对象
DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
daoProvider.setUserDetailsService(userDetailsService);
daoProvider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(daoProvider);
}
/*
* @description: 密码加密编码
* @date: 2024/4/30 0:25
* @return: org.springframework.security.crypto.password.PasswordEncoder
**/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
登录接口实现
package com.boot.controller;
import com.boot.entity.User;
import com.boot.result.Result;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController
@RequestMapping("/auth")
public class UserController {
@Autowired
private AuthenticationManager authenticationManager;
/*
* @description: 登录接口
* @param: [user]
**/
@PostMapping("/login")
public Result login(@RequestBody User user, HttpServletResponse response) {
//借助UsernamePasswordAuthenticationToken类来进行对登录数据的接收处理
UsernamePasswordAuthenticationToken
authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());
//将接收的数据交给认证管理器进行认证处理,并且在这进行登录失败的预处理
Authentication authenticate = null;
try {
authenticate = authenticationManager.authenticate(authenticationToken);
}catch (AuthenticationException e){
e.printStackTrace();
//在这如果出现异常那么就是系统自动给我们抛的认证失败异常,因此如果需要那么在这做个失败响应即可
//并且一般在认证失败后会给前端响应一个403的状态码
response.setStatus(403);
return new Result(403,"操作失败","用户账号或者密码错误!");
}
User u = (User)authenticate.getPrincipal();
//在这模拟根据用户生成一个token返回给前端
String token = UUID.randomUUID().toString();
return Result.success(token);
}
}
测试阶段
在此代码写完了,然后我们直接启动项目开始测试代码
但是启动之后出现了一个bug(O.o)
然后我便上完找了找资料,发现了问题所在,竟是mybatis-plus起步依赖的版本太低的问题,因此我们只需将其依赖版本换为3.5.5即可
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
再次启动
启动成功没有问题,然后我们到postman中进行接口测试
然后呃呃呃又出现了问题,就是用户账号密码都是对的,但是响应结果不对,虽然后面更改了restful风格,然后我找了找,发现问题出现在数据中,因为在数据库中一开始设定的用户密码是123456,并未经过加密,然而在security认证过程中对于用户密码的匹配是将数据密码进过加密后再进行匹配的,因此我们在这得手动将数据库中的用户密码给加密,因此我就快速给写了个测试方法来给它搞了加密后的密码并更新上
@Test
public void test() {
BCryptPasswordEncoder en = new BCryptPasswordEncoder();
System.out.println(en.encode("123456"));
}
然后再次进行测试
测试成功!没有问题!然后再测试认证失败的
哦耶,都没有问题!好了,该项目到这也就差不多结束了,以上就是对于本次项目的实现过程,过程中用到比较多的技术支持以及源码思路参考,希望能给读者对于Security的学习提供相关帮助,后面我会慢慢持续更新系列知识,感兴趣的可以收藏加关注,谢谢支持