添加maven依赖
第一步:添加SpringSecurity依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
第二步:添加JWT依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
准备工作
sql建表语句
create table account
(
user_id varchar(36) not null
primary key,
user_nick_name varchar(36) not null comment '用户昵称',
email varchar(36) not null comment '用户邮箱',
password varchar(255) not null comment '密码',
sex varchar(2) not null comment '性别',
birthday varchar(26) not null comment '生日',
reg_time varchar(26) not null comment '注册时间',
log_time varchar(26) not null comment '最后登录时间',
avatar_url varchar(255) not null comment '头像路径',
status int not null comment '状态'
)
row_format = DYNAMIC;
Java实体类
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import java.io.Serializable;
import com.mybatisflex.core.keygen.KeyGenerators;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;
import java.io.Serial;
/**
* 实体类。
*
* @author Matkurban
* @since 2024-05-20
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table("account")
public class Account implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Id(keyType = KeyType.Generator,value = KeyGenerators.uuid)
private String userId;
/**
* 用户昵称
*/
@Length(min = 1, max = 20, message = "昵称长度在1~20位")
private String userNickName;
/**
* 用户邮箱
*/
@Email(message = "必须是邮箱格式")
private String email;
/**
* 密码
*/
@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{6,16}$", message = "密码需要6到16位长度,且包含数字、大小写字母")
private String password;
/**
* 性别
*/
@Length(min = 1, max = 2)
private String sex;
/**
* 生日
*/
private String birthday;
/**
* 注册时间
*/
private String regTime;
/**
* 最后登录时间
*/
private String logTime;
/**
* 头像路径
*/
@URL(message = "头像地址必须为链接")
private String avatarUrl;
/**
* 状态
*/
private Integer status;
}
响应工具类
import lombok.Data;
@Data
public class ResResult<T> {
private int code;
private String msg;
private T data;
public ResResult(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> ResResult<T> success(String msg, T data) {
return new ResResult<>(200, msg, data);
}
public static <T> ResResult<T> success(String msg) {
return new ResResult<>(200, msg, null);
}
public static <T> ResResult<T> fail(String msg, T data) {
return new ResResult<>(404, msg, data);
}
public static <T> ResResult<T> fail(String msg) {
return new ResResult<>(404, msg, null);
}
}
全局异常处理类
package com.kur.kurban.config;
import com.kur.kurban.utils.ResResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandling {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public ResResult<Void> parameterAbnormality(MethodArgumentNotValidException error) {
return ResResult.fail(error.getMessage());
}
@ExceptionHandler(value = RuntimeException.class)
@ResponseBody
public ResResult<Void> runTime(RuntimeException error) {
log.error(error.getMessage());
return ResResult.fail(error.getMessage());
}
}
SpringSecurity相关实现类
UserDetails
import com.kur.kurban.entity.Account;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUserDetails implements UserDetails {
private Account account;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return account.getPassword();
}
@Override
public String getUsername() {
return account.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetailsService
import com.kur.kurban.entity.Account;
import com.kur.kurban.mapper.AccountMapper;
import com.mybatisflex.core.query.QueryWrapper;
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;
import java.util.Objects;
import static com.kur.kurban.entity.table.AccountTableDef.ACCOUNT;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final AccountMapper accountMapper;
public UserDetailsServiceImpl(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
QueryWrapper loginWrapper = new QueryWrapper();
loginWrapper.select().from(ACCOUNT).where(ACCOUNT.EMAIL.eq(username));
Account account = accountMapper.selectOneByQuery(loginWrapper);
if (Objects.isNull(account)) {
throw new RuntimeException("未查询到用户");
}
//TODO 查询对应的权限信息
return new LoginUserDetails(account);
}
}
Jwt相关
jwt工具类
生成,验证,解析token
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import java.io.Serial;
import java.util.HashMap;
import java.util.Map;
public class JwtUtils {
private static final String SECRET_KEY = "kurban"; // 应该从安全的地方加载,且足够复杂
/**
* 生成JWT令牌
*
* @param userId 用户名或其他需要保存在JWT中的信息
* @return 生成的JWT字符串
*/
public static String createToken(String userId) {
Map<String, Object> map = new HashMap<>() {
@Serial
private static final long serialVersionUID = 1L;
{
put("userId", userId);
// put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 15);
}
};
return JWTUtil.createToken(map, SECRET_KEY.getBytes());
}
/**
* 验证JWT令牌是否有效
*
* @param token JWT字符串
* @return 解码后的JWT信息,如果无效则返回null
*/
public static boolean verifyToken(String token) {
return JWTUtil.verify(token, SECRET_KEY.getBytes());
}
/**
* 解析token
*/
public static String parseToken(String token) {
JWT jwt = JWTUtil.parseToken(token);
return jwt.getPayload("userId").toString();
}
}
验证请求处理类
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.kur.kurban.security.impl.LoginUserDetails;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private final RedisTemplate<String,String> redisTemplate;
public JwtAuthenticationTokenFilter(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
System.out.println("token:"+token);
if (!StringUtils.hasText(token)) {
filterChain.doFilter(request,response);
return;
}
//解析token
if (!JwtUtils.verifyToken(token)){
throw new RuntimeException("token非法");
}
String userId = JwtUtils.parseToken(token);
if (!StringUtils.hasText(userId)){
throw new RuntimeException("token异常");
}
//从redis获取用户信息
String redisKey = "login:" + userId;
String obj = redisTemplate.opsForValue().get(redisKey);
if (!StringUtils.hasText(obj)){
throw new RuntimeException("用户未登录");
}
JSONObject jsonObject= JSONUtil.parseObj(obj);
LoginUserDetails loginUserDetails = JSONUtil.toBean(jsonObject, LoginUserDetails.class);
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(loginUserDetails,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request,response);
}
}
SpringSecurity相关处理类
AccessDeniedHandlerImpl
import cn.hutool.json.JSONUtil;
import com.kur.kurban.utils.ResResult;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
ResResult<Void> resResult = ResResult.fail("您的权限不足");
response.getWriter().write(JSONUtil.toJsonStr(resResult));
}
}
AuthenticationEntryPointImpl
import cn.hutool.json.JSONUtil;
import com.kur.kurban.utils.ResResult;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
ResResult<Void> resResult = ResResult.fail("用户认证失败");
response.getWriter().write(JSONUtil.toJsonStr(resResult));
System.out.println("用户认证失败");
}
}
SpringSecurity配置类
import com.kur.kurban.handler.AccessDeniedHandlerImpl;
import com.kur.kurban.handler.AuthenticationEntryPointImpl;
import com.kur.kurban.jwt.JwtAuthenticationTokenFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
private final AuthenticationEntryPointImpl authenticationEntryPoint;
private final AccessDeniedHandlerImpl accessDeniedHandler;
public SpringSecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter, AuthenticationEntryPointImpl authenticationEntryPoint, AccessDeniedHandlerImpl accessDeniedHandler) {
this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
this.authenticationEntryPoint = authenticationEntryPoint;
this.accessDeniedHandler = accessDeniedHandler;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
//这一行代码是用来禁用Cross-Site Request Forgery (CSRF)保护的。
// CSRF是一种网络攻击方式,通过禁用它,意味着在当前安全配置中,我们不检查请求是否携带了CSRF令牌。
// 这通常在API接口或状态无关(stateless)应用中是安全的做法,因为这些场景下CSRF攻击的风险较低
.csrf(AbstractHttpConfigurer::disable)
//这里配置了会话管理策略为STATELESS(无状态)。
// 这意味着Spring Security不会创建或使用HTTP会话来跟踪用户的状态。
// 这对于RESTful API服务特别有用,因为每个请求都应该携带所有必要的认证信息(如JWT令牌),而不是依赖于服务器端的会话。
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
//这一配置指定了对任何HTTP请求都不进行权限检查,允许所有的请求自由通过。
// 这是一种非常宽松的安全策略,通常在开发阶段或者特定的公开接口上使用。
// 在生产环境中,应根据实际情况更细致地控制不同请求的访问权限。
.authorizeHttpRequests(auth -> auth.requestMatchers("/account/login", "/account/register", "/resources/**","/email/getVerificationCode","/email/checkVerificationCode").permitAll().anyRequest().authenticated())
//添加jwt过滤器
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
//配置异常处理器
.exceptionHandling(exception -> exception.authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler));
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
throws Exception {
return configuration.getAuthenticationManager();
}
}
登录接口实现
@Override
public ResResult<String> login(Account account) {
//AuthenticationManager 进行用户认证
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(account.getEmail(), account.getPassword());
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
//如果认证没通过,给出对应的提示
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败");
}
//如果认证通过了,使用userId生成一个token返回
LoginUserDetails principal = (LoginUserDetails) authenticate.getPrincipal();
String userId = principal.getAccount().getUserId();
String token = JwtUtils.createToken(userId);
//把完整的用户信息存入redis,userId作为key
String jsonStr = JSONUtil.toJsonStr(principal);
redisTemplate.opsForValue().set("login:" + userId, jsonStr);
Map<String, String> map = new HashMap<>();
map.put("token", token);
map.put("account", JSONUtil.toJsonStr(principal.getAccount()));
return ResResult.success("登录成功", JSONUtil.toJsonStr(map));
}