SpringBoot整合SpringSecurity + JWT

news2024/12/25 23:11:52

SpringBoot整合SpringSecurity + JWT

前置知识:Cookie,Session,Token

Cookie,Session介绍

Cookie 、 Session 和 Token 是用于在 Web 应用程序中管理用户状态和身份验证的技术。因为在 Web 应用中, HTTP的通信是无状态的,每个请求都是完全独立的,所以服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。

Cookie 是由服务器发送给厍户浏览器的小型文本文件,存储在客户端的浏览器中。它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。服务器可以读取 Cookie 并使厍其中的信息来进行识别和个性化处理。

每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的。cookie 是不可跨域的,并且每个域名下面的 cookie 的数量也是有限的。

Session 是在服务器端创建和管理的一种会活机制。当用户首次访问网站时,服务器会为该用户创建一个唯一的SessionID,通常通过 Cookie 在客户端进行存储。会话标识符在后续的清求中厍于标识具体是哪个厍户。通常情况下,session 是基于 cookie 实现的, session 存储在服务器端, sessionld 会帔存储到客户端的 cookie 中。

在这里插入图片描述

有了 cookie 和 session 之后,基本可以实现用户的各种验证、鉴权及身份识别了。但是还是有一些场景中,不是特别适合这两种方案,或者说这两种方案的话还不够,那么就需要token上场了。

主要由以下几个场景:

1 、跨域请求: Cookie 是不支持跨域的,当在不同的域名之间进行通信时,使用 Token 可以更方便地在跨域清求中传递身份验证信息,而不 Cookie 限制。

2 、分布式场景: Session 是存储在服务器上的,但是现在很多都是集群部署,这就使得 Session 也需要实现分布式 Session, 而如果能用 Token 的话,就可以不用这么复杂。

3 、 **API交互:**当我们使用浏览器访问后端服务的时候,可以用 cookie 和 session, 但是如果是 API 调用,比如Dubbo 交互,就没办法做 cookie 的存储和传递了,而使用 To ken 是常见的身份验证方式。客户端通过提供Token来证明其身份,并获得对受保护资源的访问权限。

4 、跨平台应用程序: Token 可以轻松地在不同的平台和设备之间共享和传递,而无需依赖特定的会话机制或Cookie 支持。

5 、**前后端分离顶目:**现在很多项目都是前后端分离的了,这种项目中,前端和后端之间涌过 API 的方式交互,这种的话用 Token 也会更加方便一些。

Token介绍

Token 也是一种用于用户身份鉴权的手段。他其实是一种代表用户身份验证和授权的令牌。在 Web 应用程序中,常用的身份验证方案是基于令牌的身份验证 (Token-based Authentication) 。当用户成功登录时,服务器会生成一个 Token 并将其返回给客户端。客户端在后续的请求中将 Token 包含在请求头或请求参数中发送给服务器。服务器接收到 Token 后,会进行验证和解析,以确定用户的身份和权限。Token 通常是基于某种加密算法生成的,因此具有一定的安全性。

在这里插入图片描述

session与token对比

  • token 是无状态的,后端不需要记录信息,每次请求过来进行解密就能得到对应信息。
  • session 是有状态的,需要后端每次去检索id的有效性。不同的session都需要进行保存,但也可以设置单点登录,减少保存的数据。
  • session与token的选择是空间与时间博弈,为什么这么说呢,是因为token不需要保存,不占存储空间,但每次访问都需要进行解密,消耗了一定的时间。在一般的前后端分离项目中,token展现出了它的优势,成为了比session更好的选择。

JWT

JWT其实就是一种被广泛使用的token,它的全称是JSON Web Token,它通过数字签名的方式,以JSON对象为载体,在不同的服务终端之间安全地传输信息。
  JWT最常见的使用场景就是授权认证,一旦用户登录,后续每个请求都将包含JWT,系统在每次处理用户请求之前,都要先进行JWT安全校验,通过之后再进行处理。
  JWT由3部分组成,用.拼接,如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
  这三部分分别是:

**Header:**Header中保存了令牌类型type和所使用的的加密算法,例如:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

**Payload:**Payload中包含的是请求体和其它一些数据,例如包含了和用户相关的一些信息

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Signature:Signature签名属于 JWT 的第三部分。主要是把Header的base64UrlEncode与Payload的base64UrlEncode拼接起来,再进行HMACSHA256加密等最终得到的结果作为签名部分。例如:

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

SpringSecurity

SpringSecurity 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。

一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。

一般Web应用的需要进行认证和授权。

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:经过认证后判断当前用户是否有权限进行某个操作

而认证和授权也是SpringSecurity作为安全框架的核心功能。

SpringBoot整合SpringSecurity及JWT

1、添加pom依赖

<!-- springboot security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>${jwt.version}</version>
</dependency>

2、添加JWT工具类

用于生成解析token

application.yml

# token配置
token:
  # 令牌自定义标识
  header: Authorization
  # 令牌密钥
  secret: fhgaf%^$#%cHDFDhUHLKnhkhj
  # 令牌有效期(默认30分钟)
  expireTime: 2628000

JwtUtils

@Data
@Component
@ConfigurationProperties(prefix = "token")
public class JwtUtils {

    private long expireTime;

    private String secret;

    private String header;

    protected static final long MILLIS_SECOND = 1000;

    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;

    private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
    // 生成JWT
    public String generateToken(String account) {

        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + MILLIS_SECOND * expireTime);

        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(account)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    // 解析JWT
    public Claims getClaimsByToken(String jwt) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(jwt)
                    .getBody();
        } catch (Exception e) {
            return null;
        }
    }

    // 判断JWT是否过期
    public boolean isTokenExpired(Claims claims) {
        return claims.getExpiration().before(new Date());
    }

}

3、添加ResponseResult类统一接口返回结果

import java.util.HashMap;

/**
 * @Classname ResponseResult
 * @Description: 请求响应模板
 * @Author: zengxi
 * @CreateDate: 2023/11/28 19:32
 */
public class ResponseResult extends HashMap<String, Object> {
    private static final long serialVersionUID = 1L;

    private String MESSAGE_TAG = "message";

    private String CODE_TAG = "code";

    private String DATA_TAG = "data";

    /**
     * 初始化一个新创建的 ResponseResult 对象,使其表示一个空消息。
     */
    public ResponseResult() {
    }

    /**
     * 初始化一个新创建的 ResponseResult 对象
     *
     * @param code    状态码
     * @param message 返回内容
     */
    public ResponseResult(int code, String message) {
        super.put(CODE_TAG, code);
        super.put(MESSAGE_TAG, message);
    }

    /**
     * 初始化一个新创建的 ResponseResult 对象
     *
     * @param code    状态码
     * @param message 返回内容
     * @param data    返回数据
     */
    public ResponseResult(int code, String message, Object data) {
        super.put(CODE_TAG, code);
        super.put(MESSAGE_TAG, message);
        if (data != null) {
            super.put(DATA_TAG, data);
        }
    }

    /**
     * 返回成功消息
     */
    public static ResponseResult success() {
        return new ResponseResult(200, "操作成功", null);
    }

    /**
     * 返回成功消息
     *
     * @param data 返回数据
     */
    public static ResponseResult success(Object data) {
        return new ResponseResult(200, "操作成功", data);
    }

    /**
     * 返回成功消息
     *
     * @param message 返回内容
     */
    public static ResponseResult success(String message) {
        return new ResponseResult(200, message, null);
    }

    /**
     * 返回成功消息
     *
     * @param message 返回内容
     * @param data    返回数据
     */
    public static ResponseResult success(String message, Object data) {
        return new ResponseResult(200, message, data);
    }

    /**
     * 返回失败消息
     */
    public static ResponseResult error() {
        return new ResponseResult(500, "操作失败", null);
    }

    /**
     * 返回失败消息
     *
     * @param message 返回内容
     */
    public static ResponseResult error(String message) {
        return new ResponseResult(500, message, null);
    }

    /**
     * 返回失败消息
     *
     * @param data 返回内容
     */
    public static ResponseResult error(Object data){
        return new ResponseResult(500, "操作失败", data);
    }

    /**
     * 返回失败消息
     *
     * @param message 返回内容
     * @param data 返回数据
     */
    public static ResponseResult error(String message, Object data) {
        return new ResponseResult(500, message, data);
    }

    /**
     * 方便链式调用
     *
     * @param key 键
     * @param value 值
     * @return 数据对象
     */
    @Override
    public ResponseResult put(String key, Object value)
    {
        super.put(key, value);
        return this;
    }
}

3、实现UserDetailsService接口,重写其中的方法

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.easynote.common.domain.LoginUser;
import com.example.easynote.module.user.domain.User;
import com.example.easynote.module.user.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
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 javax.annotation.Resource;
import java.util.Objects;

/**
 * @Classname UserDetailsServiceImpl
 * @Description: 实现springSecurity UserDetailsService接口
 * @Author: zengxi
 * @CreateDate: 2024/1/11 20:30
 */
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
        // 根据用户名查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getAccount,account);
        User user = userMapper.selectOne(wrapper);
        // 如果查询不到数据就通过抛出异常来给出提示
        if(Objects.isNull(user)){
            log.info("登录用户:{} 不存在",account);
            throw new RuntimeException("登录用户:" + account + "不存在");
        }
        // 封装成UserDetails对象返回
        return new LoginUser(user);
    }
}

我们可以看到重写的方法loadUserByUsername返回的结果是SpringSecurity中的UserDetails接口

在这里插入图片描述

所以我们要将自己的User对象转换为UserDetails,故我们写了一个LoginUser类实现了UserDetails

import com.example.easynote.module.user.domain.User;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @Classname LoginUser
 * @Description: 实现UserDetails
 * @Author: zengxi
 * @CreateDate: 2024/1/11 20:35
 */
@Data
public class LoginUser implements UserDetails {

    private static final long serialVersionUID = 1L;

    private User user;

    public LoginUser(){
    }

    public LoginUser(User user){
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getAccount();
    }

    /**
     * 账户是否未过期,过期无法验证
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否可用 ,禁用的用户不能身份验证
     */
    @Override
    public boolean isEnabled()
    {
        return true;
    }

}

4、实现登录逻辑

import com.example.easynote.common.domain.LoginUser;
import com.example.easynote.common.domain.ResponseResult;
import com.example.easynote.common.utils.JwtUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Objects;

/**
 * @Classname LoginServcie
 * @Description: 登录逻辑实现
 * @Author: zengxi
 * @CreateDate: 2024/1/11 21:34
 */
@Service
public class LoginServcie {
    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private JwtUtils jwtUtils;

    public String login(String account, String password) {
        UsernamePasswordAuthenticationToken 
            authenticationToken = new UsernamePasswordAuthenticationToken(account, password);
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("用户名或密码错误");
        }
        // 使用account生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getUserId().toString();
        return jwtUtils.generateToken(account);
    }
}

注意:我们在LoginServcie是通过AuthenticationManagerauthenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

package com.example.easynote.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * @Classname SecurityConfig
 * @Description: SpringSecurity配置类
 * @Author: zengxi
 * @CreateDate: 2024/1/11 21:07
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }
}

5、登录接口

此时我们通过调用LoginServcie中的login方法,来实现登录接口了

import com.example.easynote.common.controller.BaseController;
import com.example.easynote.common.domain.ResponseResult;
import com.example.easynote.common.domain.page.TableDataInfo;
import com.example.easynote.common.service.LoginServcie;
import com.example.easynote.module.user.domain.User;
import com.example.easynote.module.user.domain.UserDTO;
import com.example.easynote.module.user.service.UserService;
import com.github.pagehelper.PageInfo;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;


/**
 * @Classname UserController
 * @Description: UserController
 * @Author: zengxi
 * @CreateDate: 2023/11/28 19:14
 */

@RestController
@RequestMapping("/user")
public class UserController extends BaseController {

    @Resource
    UserService userService;

    @Resource
    LoginServcie loginServciel;

    @PostMapping("/login")
    public ResponseResult login(@RequestBody UserDTO user) {
        String token = loginServciel.login(user.getAccount(), user.getPassword());
        return ResponseResult.success(token);
    }
}

此时我们访问登录接口时发现,接口无返回结果,请求状态403,并且后端项目日志也无任何输出

在这里插入图片描述

这是SpringSecurity将我们的登录接口拦截了,只需在SecurityConfig,放行登录接口即可

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                // .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("user/login", "/user/register", "/captchaImage").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/profile/**"
                ).permitAll()
                .antMatchers("/swagger-ui.html").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }
}

此时我们再访问登录接口,SpringSecurity放行了,但是报了如下错误

在这里插入图片描述

这是由于在Spring Security 5.xx版本中新增了加密方式的校验。之前版本中的NoOpPasswordEncoderDelegatingPasswordEncoder取代了,由于在新建用户的时候,没有指定密码的加密方式。但是现有版本的Spring Security会对前端传来的密码进行检查,检验密码格式是否符合格式**{id}encodedPassword**,在DelegatingPasswordEncoder源码中给出了不同加密的样例,都是**{id}encodedPassword**的格式:

// {bcrypt}:BCrypt强哈希方法
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
// {noop}:无加密
{noop}password
// {PBKDF2}:PBKDF2加密
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
// {scrypt}:scrypt加密
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
// {sha256}:sha256加密
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

解决方法:

一、修改数据库密码存储方式

将数据入库时,将存储的密码改为如下格式:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

二、修改UserDetails实现中的获取密码方法

import com.example.easynote.module.user.domain.User;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @Classname LoginUser
 * @Description: 实现UserDetails
 * @Author: zengxi
 * @CreateDate: 2024/1/11 20:35
 */
@Data
public class LoginUser implements UserDetails {

    @Override
    public String getPassword() {
        // return "{noop}"+user.getPassword();
        // return "{pbkdf2}"+user.getPassword();
        // return "{scrypt}"+user.getPassword();
        // return "{sha256}"+user.getPassword();
        return "{bcrypt}"+user.getPassword();
    }
}

三、修改SecurityConfigSpringSecurity配置类(推荐)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @Classname SecurityConfig
 * @Description: SpringSecurity配置类
 * @Author: zengxi
 * @CreateDate: 2024/1/11 21:07
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 密码加密方式
     */
    @Bean
    public PasswordEncoder bCryptPasswordEncoder()
    {
    	 //不使用密码加密
        //return NoOpPasswordEncoder.getInstance();
 
        //使用默认的BCryptPasswordEncoder加密方案
        return new BCryptPasswordEncoder();
 
        //strength=10,即密钥的迭代次数(strength取值在4~31之间,默认为10)
        //return new BCryptPasswordEncoder(10);
 
        //利用工厂类PasswordEncoderFactories实现,工厂类内部采用的是委派密码编码方案.
        //return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

6、注册接口

注册逻辑

**注意:**注册使用的密码加密方式,要和之前配置的加密方式一致

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.easynote.common.utils.StringUtils;
import com.example.easynote.module.user.domain.User;
import com.example.easynote.module.user.domain.UserDTO;
import com.example.easynote.module.user.mapper.UserMapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import javax.annotation.Resource;
import java.util.Objects;

/**
 * @Classname RegisterService
 * @Description: 注册功能逻辑实现
 * @Author: zengxi
 * @CreateDate: 2024/1/12 14:13
 */
@Service
public class RegisterService {

    @Resource
    UserMapper userMapper;

    public String register(UserDTO userDTO) {
        String msg = "", account = userDTO.getAccount(), password = userDTO.getPassword();
        LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper();
        userLambdaQueryWrapper.eq(User::getAccount, account);
        if (StringUtils.isEmpty(account)) {
            msg = "账户不能为空";
        } else if (StringUtils.isEmpty(password)) {
            msg = "密码不能为空";
        } else if (Objects.nonNull(userMapper.selectOne(userLambdaQueryWrapper))) {
            msg = "注册用胡" + account + "失败,该用户已存在";
        } else {
            User user = new User();
            user.setAccount(account);
            // 对密码进行加密
            BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            String encodePassword = passwordEncoder.encode(password);
            user.setPassword(encodePassword);
            user.setNickname(userDTO.getNickname());
            user.setEmail(userDTO.getEmail());
            user.setPhone(userDTO.getPhone());
            user.setAvatar(userDTO.getAvatar());
            user.setIntroduction(userDTO.getIntroduction());
            if (userMapper.insert(user) > 0) {
                msg = "注册成功";
            } else {
                msg = "注册失败,请联系系统管理人员";
            }
        }
        return msg;
    }


}

接口
@PostMapping("/register")
public ResponseResult register(@RequestBody UserDTO user) {
    String msg = registerService.register(user);
    return ResponseResult.success(msg);
}

认证

在有些情况下,我们需要用户登录后,才能访问某些接口。这时候就需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析。

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private JwtUtils jwtUtils;

    @Resource
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = this.getTokenFromRequest(request);
        if (StringUtils.isNotEmpty(token)) {
            // 从 token 获取 username
            String username = jwtUtils.getUsernameFromToken(token);
            boolean tokenExpired = jwtUtils.isTokenExpired(jwtUtils.parseToken(token));
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (StringUtils.isNotNull(userDetails) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // 设置用户到当前线程,以保证后续操作需要用户时能直接获取到用户信息
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        filterChain.doFilter(request, response);
    }

    /**
    * @description: 截取token
    * @author ZengXi
    * @date 2024/7/24
    * @param request
    * @return java.lang.String
    */
    private String getTokenFromRequest(HttpServletRequest request){
        String bearerToken = request.getHeader("Authorization");
        if(StringUtils.isNotEmpty(bearerToken) && bearerToken.startsWith("Bearer ")){
            return bearerToken.substring(7, bearerToken.length());
        }
        return bearerToken;
    }
}

然后将过滤器添加到SecurityConfig

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * token认证过滤器
     */
    @Resource
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 跨域过滤器
     */
    @Resource
    private CorsFilter corsFilter;

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public PasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 登出logout 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/api/user/login", "/api/user/logout", "/api/user/register", "/captchaImage").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/profile/**"
                ).permitAll()
                .antMatchers("/swagger-ui.html").anonymous()
//                .antMatchers("/api/**").permitAll() // 临时用于调试
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        // httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //  将JWT filter和LogoutFilter添加到CORS filter之前
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

如果还想要自定义认证失败后怎么处理,可以实现AuthenticationEntryPoint

/**
 * 认证失败处理类 返回未授权
 * 
 * @author zengxi
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
    private static final long serialVersionUID = -8870718413537077606L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
            throws IOException {
        int code = HttpStatus.UNAUTHORIZED;
        String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
        String jsonStr = JSONUtil.toJsonStr(ResponseResult.error(code, msg));
        ServletUtils.renderString(response, jsonStr);
    }
}

然后将AuthenticationEntryPointImpl添加到SecurityConfig

	/**
     * 认证失败处理类
     */
    @Resource
    private AuthenticationEntryPointImpl unauthorizedHandler;
	@Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 登出logout 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/api/user/login", "/api/user/logout", "/api/user/register", "/captchaImage").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/profile/**"
                ).permitAll()
                .antMatchers("/swagger-ui.html").anonymous()
//                .antMatchers("/api/**").permitAll() // 临时用于调试
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        // httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //  将JWT filter和LogoutFilter添加到CORS filter之前
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

授权

在SpringSecurity中,会使用默认的拦截器FilterSecurityInterceptor来进行权限校验。

在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然

后获取其中的权限信息,最后判断当前用户是否拥有访问当前资源所需的权限。

因此在项目中只需要把当前登录用户的权限信息也存入Authentication

然后设置我们的资源所需要的权限即可。

原文链接:https://blog.csdn.net/qq_44749491/article/details/131469886

1、开启配置

开启配置需要再在SpringSecurity的配置类中增加如下注解:

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
2、封装权限信息

LoginUser中添加permissionsauthorities,并重写getAuthorities方法

/**
 * @Classname LoginUser
 * @Description: 实现UserDetails
 * @Author: zengxi
 * @CreateDate: 2024/1/11 20:35
 */
@Data
public class LoginUser implements UserDetails {

    private static final long serialVersionUID = 1L;

    private User user;

    List<String> permissions;

    public LoginUser(User user, List<String> permissions){
        this.user = user;
        this.permissions = permissions;
    }

    //存储SpringSecurity所需要的权限信息的集合
    @JsonIgnore
    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null){
            return authorities;
        }
        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        authorities = permissions.stream().
                map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }
}

UserDetailsServiceImpl中的loadUserByUsername方法中添加获取用户权限的代码

@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
    // 根据用户名查询用户信息
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(User::getAccount,account);
    User user = userMapper.selectOne(wrapper);
    // 如果查询不到数据就通过抛出异常来给出提示
    if(Objects.isNull(user)){
        log.info("登录用户:{} 不存在",account);
        throw new RuntimeException("登录用户:" + account + "不存在");
    }
    // 获取用户权限列表,这里只是为了测试,在真正的开发中,需要从数据库李查询
    List<String> permissions = new ArrayList<>(Arrays.asList("permissions","ROLE_test"));
    return new LoginUser(user, permissions);
}
3、测试请求类
@RestController
@RequestMapping("/api/test")
@Slf4j
public class TestController {

    @PreAuthorize("hasAuthority('permissions')")
    @GetMapping("/permissions")
    public String permissions() {
        return "permissions";
    }
	
    @PreAuthorize("hasAuthority('permissions2')")
    @GetMapping("/permissions2")
    public String permissions2() {
        return "permissions2";
    }
    
    // hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。
    @PreAuthorize("hasAnyAuthority('permissions','permissions2')")
    @GetMapping("/permissions2")
    public String permissions2() {
        return "permissions2";
    }
    
    // hasRole,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较
    @PreAuthorize("hasRole('test')")
    @GetMapping("/permissions3")
    public String permissions3() {
        return "permissions3";
    }

    // hasAnyRole,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较
    @PreAuthorize("hasAnyRole('admin','test')")
    @GetMapping("/permissions3")
    public String permissions3() {
        return "permissions3";
    }
}
4、自定义授权失败处理实现类
/**
 * @Description 授权失败自定义类
 * @Author ZengXi
 * @Date 2024/8/28
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN, "权限不足");
        String jsonStr = JSONUtil.toJsonStr(result);
        ServletUtils.renderString(response, jsonStr);
    }
}

添加到SecurityConfig

@Resource
private AccessDeniedHandlerImpl accessDeniedHandler;
httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2081058.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于单片机的智能防盗窗户的设计

本设计是一种基于单片机的智能防盗窗户&#xff0c;采用STC89C52单片机作为核心控制器&#xff0c;配合DHT11温湿度传感器和MQ-2烟雾传感器等传感器和模块&#xff0c;用于收集室内环境数据&#xff0c;并通过单片机进行数据处理和控制。实现窗户状态的智能监测和防盗报警&…

CSEC:香港城市大学提出SOTA曝光矫正算法 | CVPR 2024

在光照条件不佳下捕获的图像可能同时包含过曝和欠曝。目前的方法主要集中在调整图像亮度上&#xff0c;这可能会加剧欠曝区域的色调失真&#xff0c;并且无法恢复过曝区域的准确颜色。论文提出通过学习估计和校正这种色调偏移&#xff0c;来增强既有过曝又有欠曝的图像。先通过…

HR人力资源OKR示例

O&#xff1a;提高内部员工敬业度和工作满意度 KR1: 开展9月每周五全体员工的会议与励志演讲主题 KR2: 采访36名员工&#xff0c;了解他们改善工作文化的需求 KR3: 在所有16个部门中使用OKR和Tita软件实施 KR4: 达到至少每周员工的满意度&#xff1a;4.7分 O&#xff1a;招聘工…

如何确定电脑与PLC有没有链接上

直接Ping一下 第一步&#xff1a;winR调出运行框 第二步&#xff1a;输入CMD-点确认 弹出运行框在里面输入Ping 192.168.10.1 失败状态 成功状态

关于Flink内存分配核心知识点

这个问题同样也是之前辅导过的同学的面试问题&#xff0c;这个问题非常接地气且考察面试者的实践经验。事实上&#xff0c;这也是我们大数据提高班的Flink专项提高部分内容。 下面我列举的这些就是核心&#xff0c;能答出这些重点即可。 内存模型在Flink1.9和Flink1.11版本做了…

EmguCV学习笔记 VB.Net 7.1 角点检测

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 EmguCV是一个基于OpenCV的开源免费的跨平台计算机视觉库,它向C#和VB.NET开发者提供了OpenCV库的大部分功能。 教程VB.net版本请访问…

【React】跨域问题详解及解决方案

文章目录 一、什么是跨域问题&#xff1f;1. 同源策略的定义2. CORS 机制 二、在 React 项目中遇到的跨域问题常见的跨域错误信息 三、解决 React 中跨域问题的方法1. 在后端服务器上配置 CORS2. 在 React 项目中使用代理 (Proxy)2.1 使用 http-proxy-middleware 实现代理1. 安…

SAP主数据删除

项目场景&#xff1a; SAP项目上线初期&#xff0c;主数据批到相关的操作 操作描述 需要对供应商&#xff0c;客户&#xff0c;科目进行维护操作&#xff0c;比如这些数据创建错误&#xff0c;不想留一些垃圾数据在系统里面 解决方案&#xff1a; 事务代码&#xff1a;OBR2…

低侧与高侧电流检测对比

1 简介 在处理低至中等电流水平时&#xff0c;电阻电流检测广泛用于印刷电路板组件。使用这种技术&#xff0c;将一个已知的电阻 R分流器与负载串联&#xff0c;并测量电阻两端的电压以确定负载电流。如下图所示。 电流检测电阻器&#xff0c;也称为分流电阻器或简称为分流器&a…

Python TensorFlow实战篇

概述 本篇博客将详细介绍如何使用Python和TensorFlow解决实际问题&#xff0c;包括图像分类、序列预测以及模型部署等内容。我们将从以下几个方面进行深入探讨&#xff1a; 图像分类实战&#xff1a;使用卷积神经网络&#xff08;CNN&#xff09;进行图像分类。序列预测实战&…

模型 KT决策法

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。系统分析&#xff0c;明智选择。 1 KT决策法的应用 1.1 餐饮连锁店菜单更新 一家餐饮连锁店计划更新菜单&#xff0c;以吸引更多顾客并提高销售额。使用 KT 决策法&#xff08;Kepner-Tregoe&#x…

哪些领域最适合采用音视频私有化解决方案?

随着数字化时代的到来&#xff0c;音视频通信已成为各行各业不可或缺的一部分&#xff0c;从企业内部沟通到在线教育、远程医疗、金融交易等&#xff0c;无一不依赖于稳定、高效且安全的音视频技术。然而&#xff0c;不同的行业对音视频通信的需求各不相同&#xff0c;尤其在数…

【Denuvo加密】黑神话悟空为什么没有破解版?Denuvo加密技术的详细解析与代码示例

文章目录 1. 引言2. 加密技术概述3. D加密技术的工作原理4. D加密技术的实现5. D加密技术的实际应用6.实现原理7. 本篇小结 更多相关内容可查看 1. 引言 随着游戏产业的蓬勃发展&#xff0c;游戏数据的保护成为了重要议题。《黑神话&#xff1a;悟空》作为一款备受期待的游戏&…

大型商业中心的绿色转型之路

在全球可持续发展浪潮的推动下&#xff0c;大型商业中心正悄然蜕变&#xff0c;从能源消耗大户转变为绿色运营的先锋。在这个转型的舞台上&#xff0c;商场电气管理者们以创新的智慧与坚定的决心&#xff0c;绘制出一幅幅节能减排、低碳生活的美好蓝图。 面对大型商业中心复杂…

职业本科物联网与智能感知实训室解决方案

一、前言 在当今这个数字化、智能化飞速发展的时代&#xff0c;物联网&#xff08;IoT&#xff09;与智能感知技术已成为推动产业升级、促进经济社会发展的重要力量。为了适应这一趋势&#xff0c;培养具备物联网技术应用与智能感知系统设计能力的高素质技术技能型人才&#xf…

遗传算法与深度学习实战(8)——使用遗传算法解决旅行商问题

遗传算法与深度学习实战&#xff08;8&#xff09;——使用遗传算法解决旅行商问题 0. 前言1. 旅行商问题2. NP 问题3. 构建 TSP 求解器小结系列链接 0. 前言 旅行商问题 (Traveling Salesman Problem, TSP) 是一个经典的优化问题&#xff0c;其目标是找到一条最短的路径&…

280Hz显示器怎么选

280Hz显示器怎么选&#xff1f;今天就给大家带来6大品牌和型号的280Hz显示器一起对比对比&#xff01; 1.280Hz显示器 - HKC G27H3显示器 当电竞遇上显示器&#xff0c;就像是超级英雄找到了他的战衣&#xff0c;完美搭配&#xff0c;所向披靡。今天&#xff0c;我们要聊的这款…

XSS LABS - Level 15 过关思路

关注这个靶场的其他相关笔记&#xff1a;XSS - LABS —— 靶场笔记合集-CSDN博客 0x01&#xff1a;过关流程 进入靶场&#xff0c;老流程&#xff0c;右击查看网页源码&#xff0c;看看有没有接收传参并回显的位置&#xff1a; 可以发现&#xff0c;src 接收的参数被回显了&am…

探索未来交互——Open LLM VTuber:一款基于AI大模型的二次元虚拟主播

随着人工智能技术的飞速发展,虚拟主播(VTuber)行业迎来了全新的变革。本文将介绍一个令人兴奋的开源项目——Open LLM VTuber,这是一个本地运行的、可高度定制的虚拟主播平台,它不仅支持多种语言模型(LLM)、自动语音识别(ASR)和文本转语音(TTS)后端,而且能够跨操作…

SingleChildScrollView使用

Flutter 中&#xff0c;SingleChildScrollView&#xff08;类比Android中的ScrollView&#xff09; 是一个可以滚动单个子控件的小部件。当子控件的大小超过视图时&#xff0c;用户可以滚动以查看所有内容。SingleChildScrollView 通常用于创建可滚动的表单、列表或任何需要垂直…