SpringBoot3整合SpringSecurity

添加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));
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JsonToDart

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值