SpringSecurity的使用

SpringSecurity

本篇文章基于自定义的User表来做用户登陆校验与权限访问。 并且基于SpringBoot三层框架来实现。包含使用JWT来做登陆验证和身份权限验证,控制路由资源访问。

因为看官方文档不是很看的懂,并且整套配置流程也比较复杂,在github上又都是一些复杂的项目,代码很难看懂,所以专门写一个文档出来做流程记录。

自定义user表如下

kotlin 复制代码
package yellow.iblog.model;

import com.baomidou.mybatisplus.annotation.*;

import lombok.AllArgsConstructor;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("users")
@AllArgsConstructor
public class User {
    @TableId(type= IdType.ASSIGN_ID)
    private Long uid;
    @TableField("user_name")
    private String userName;
    @TableField("gender")
    private Character gender;

    @TableField("age")
    private Integer age;

    @TableField("password") //存储hash值,需要长一点
    private String password;

    @TableField("role")
    private String role;//集成SpringSecurity,值以ROLE_开头(ROLE_USER,ROLE_ADMIN)

    // 新增创建时间字段
    @TableField(value = "created_at",fill=FieldFill.INSERT)
    private LocalDateTime createdAt;

    // 新增更新时间字段
    @TableField(value="updated_at",fill=FieldFill.INSERT_UPDATE)
    private LocalDateTime updatedAt;
    //构造函数
    public User( String userName, Character gender, Integer age, String password) {
        this.userName = userName;
        this.gender = gender;
        this.age = age;
        this.password = password;
        this.role="ROLE_USER";
    }

    public User(String userName, Character gender, Integer age) {
        this.userName = userName;
        this.gender = gender;
        this.age = age;
        this.role="ROLE_USER";
    }

    public User(){}

}

SpringSecurity的作用:

1.身份验证(登录/未登录)

2.授权(控制谁可以访问哪些资源)普通用户、VIP、admin

3.防止攻击

做法

1.引入依赖

xml 复制代码
<!--        JWT-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
<!-- Security:哈希加密、认证授权 添加这个依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2.User里面添加role字段

并且只能以ROLE_USER的格式赋值(SpringSecurity的规定)

并且UserName要设置为unique,不然就要用手机号或者邮箱登陆,邮箱还好搞,手机号要上阿里云或其它云那里买服务

默认值设置为ROLE_USER

ini 复制代码
  @TableField("role")
  private String role;//集成SpringSecurity,值以ROLE_开头(ROLE_USER,ROLE_ADMIN)
   //构造函数
    public User( String userName, Character gender, Integer age, String password) {
        this.userName = userName;
        this.gender = gender;
        this.age = age;
        this.password = password;
        this.role="ROLE_USER";
    }
​
    public User(String userName, Character gender, Integer age) {
        this.userName = userName;
        this.gender = gender;
        this.age = age;
        this.role="ROLE_USER";
    }

问题来了:既然已经有role字段,为什么不在校验的时候直接接收前端发过来的role做校验?为什么还要搞一个jwt来读取校验?

---因为前端发过来的话容易被篡改,并且发请求谁不会啊,一下就给你发个修改Role的请求,给自己整成管理员了,然后全部用户都删光。这样不安全。从上下文读取的话,就会安全,不容易被篡改了。

3.集成JWT环境

因为权限认证从JWT字符串里面解析出role的值,然后再做校验,所以要先搞好JWT

(1)新建一个jwt包,并在里面写jwt的工具类

我的项目比较小,所以单独开了一个jwt包,规范一点可以写在工具类包里面

jwtUtils.java

要包括生成jwt、解析jwt(校验密钥,获取claims身份信息,角色信息)、校验jwt(就是看解析的过程有没有抛出异常)

java 复制代码
package yellow.iblog.jwt;
​
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
​
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
​
​
public class jwtUtils {
    // 生成一个秘钥(也可以配置在 application.yml)
    //这里是随机生成一个密钥
//    private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private static final String SECRET = "emily-is-gonna-be-rich-888888888888888888"; // 必须至少32字节(32个字符)
    private static final Key key = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
​
    private static final long EXPIRATION = 1000 * 60 * 60; // 1小时(以ms为单位)
​
    // 生成 token
    public static String generateToken(Long uid, String username,String role) {
        return Jwts.builder()
                .setSubject(uid.toString())//这个使用uid是因为这个uid是永远不变的,userName还可能会改变
                .claim("role", role)//添加构成jwt的参数
                .claim("username",username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(key)
                .compact();//压缩的意思
    }
​
    // 解析 token
    public static Claims parseToken(String token) {
        return Jwts.parserBuilder() // 创建 JWT 解析器建造器
                .setSigningKey(key) // 验证签名密钥,如果失败会抛出异常
                .build() // 构建解析器
                .parseClaimsJws(token) // 解析 Token
                .getBody(); // 获取 Token 的载荷 (Claims) 部分
    }
​
    // 校验 token 是否有效
    public static boolean validateToken(String token) {
        try {
            parseToken(token); // 尝试解析 Token,并且验证密钥
            return true; // 如果没有异常,说明 Token 有效
        } catch (JwtException e) {
            return false; // 如果捕获到异常,说明 Token 无效(签名错误或过期)
        }
    }
}
​
jwt中subject的含义:

setSubject() 用来存核心身份(谁在用这个 Token)

claims.put() 用来存扩展信息(这个人有什么属性/权限)

subject就是这个用户在jwt字符串里的唯一标识,标识一个唯一的用户,所以是不能改变的

剩下的就是设置一些其它的附属信息,但也很重要,需要设置用户名,角色。这些是写进jwt字符串里面的,后面登陆的时候带上这个jwt,就可以获取到你的用户名和角色,就可以做身份验证。

还需要设置过期时间,签发日期,密钥,最后compact压缩生成jwt。

(2)新建一个JwtAuthenticationFilter类

用来拦截发过来的请求,并且每次都看你有没有带jwt?你的jwt有没有过期?身份是不是可以访问资源?

ini 复制代码
package yellow.iblog.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.Claims;
import yellow.iblog.Common.ApiResponse;
​
import java.io.IOException;
import java.util.Collections;
import java.util.List;
​
@Component //可以装配
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
​
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            if (jwtUtils.validateToken(token)) {
                Claims claims = jwtUtils.parseToken(token);
                String username=claims.getSubject();
                String role=(String) claims.get("role");
​
                // 这里你可以把用户信息放到 request
                request.setAttribute("uid", claims.get("uid"));
                request.setAttribute("username", claims.getSubject());
                request.setAttribute("role",claims.get("role"));
                // 构造 Spring Security 的认证对象
                List<GrantedAuthority> authorities = Collections.singletonList(
                        new SimpleGrantedAuthority(role));
                //这个list就是一个数组,里面的数据类型是角色字符串(GrantedAuthority) 右边的是在将从jwt中解析出来的role构造成一个List,赋值给authorities,所以authorities就是一个装着发过来请求的角色的List
              
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(username, null, authorities);
              //这个左边的类名好长一串,就是使用UserName和密码来做用户验证的,密码我们在登陆接口认证,那个接口不用jwt的。在那里认证,这里就写null,而且我们也没有把密码写进jwt里面,这里也获取不了。这个就是得到一个认证对象。表示你这个jwt的用户名已经认证了,已经登陆了。第三个参数是刚刚生成的角色列表authorities
​
              
                // 将认证信息存入 Spring Security 的上下文,后面可以通过SecurityContextHolder.getContext().getAuthentication() 拿到用户信息。
                SecurityContextHolder.getContext().setAuthentication(authentication);
​
            } else {
                //token不对或者没有token
                ApiResponse<Object> apiResponse = ApiResponse.fail("您没有足够的权限或者登陆已经过期,请联系工作人员");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(new ObjectMapper().writeValueAsString(apiResponse));
                return;
​
            }
        }
​
        filterChain.doFilter(request, response);
    }
}
​

继承OncePerRequestFilter就可以拦截每一个请求

这个就是在解析你携带的jwt,并且保存到Authentication里面

后续获取Authentication的方法:

ini 复制代码
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 用户名(principal)
String username = (String) auth.getPrincipal();
// 权限(角色)
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
// 也可以遍历权限
for (GrantedAuthority authority : authorities) {
    System.out.println("角色: " + authority.getAuthority());
}

4.写用户登陆的Sercive

(1)在UserService里面定义login接口

(2)在UserServiceImpl里面写登陆验证逻辑

传进去用户名和密码,如果密码正确,那么发token

scss 复制代码
   //用户登陆
    @Override
    public ApiResponse<LoginResponse> userLogin(LoginInfo loginInfo){
        String userName=loginInfo.getUserName();
        User u=userMapper.findUserByUserName(userName);
        if(u==null) return ApiResponse.fail("用户名不存在");//如果没有查到user就返回用户名不存在
        //校验密码
        String password=loginInfo.getPassword();
        if(utils.Match(password,u.getPassword())){
            //生成token
            String token=jwtUtils.generateToken(u.getUid(),u.getUserName(),u.getRole());
            LoginResponse response=new LoginResponse();
            response.setUserName(userName);
            response.setUid(u.getUid());
            //token
            response.setToken(token);
            return ApiResponse.success(response);
        }
        return ApiResponse.fail("密码不正确");//如果密码输错了,那么就返回失败
​
    }

返回的这个LoginResponse是这样的

kotlin 复制代码
package yellow.iblog.model;
​
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
​
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginResponse {
    private Long uid;
    private String userName;
    private String token;
}
​

这个apiresponse是自己写的响应类

typescript 复制代码
package yellow.iblog.Common;
​
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
​
​
@NoArgsConstructor
@Getter
@Setter
public class ApiResponse<T> {
//给前端的标准返回格式
    private int code;       // 状态码: 0=成功, 非0=失败
    private String message; // 错误提示 / 说明信息
    private T data;         // 返回的数据
​
    // 构造方法
    public ApiResponse(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }
​
    // ✅ 成功
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(0, "success", data);
    }
​
    // ✅ 自定义成功提示
    public static <T> ApiResponse<T> success(String message, T data) {
        return new ApiResponse<>(0, message, data);
    }
​
    // ✅ 失败
    public static <T> ApiResponse<T> fail(String message) {
        return new ApiResponse<>(-1, message, null);
    }
​
    // ✅ 自定义错误码 + 消息
    public static <T> ApiResponse<T> fail(int code, String message) {
        return new ApiResponse<>(code, message, null);
    }
​
    public static <T> ApiResponse<Exception> fail(int code,String message, Exception e) {
        return new ApiResponse<>(code,message,e);
    }
    public boolean IsSuccess(){
        return this.getCode() >= 0;
​
    }
}

5.写用户登陆的Controller

less 复制代码
  //用户登陆,使用POST更加安全
  @PostMapping("/auth/login")
  public ResponseEntity<ApiResponse<LoginResponse>> userLogin(@RequestBody LoginInfo loginInfo){
      ApiResponse<LoginResponse> result=userService.userLogin(loginInfo);
      if(result.IsSuccess()){
          log.info("用户{}登陆成功",loginInfo.getUserName());
          return ResponseEntity.ok(result);
​
      } else{
          log.warn("用户{}登陆失败",loginInfo.getUserName());
          return ResponseEntity.badRequest().body(result);
      }
  }

LoginInfo就是包含用户名和密码的一个结构体

6.配置路由访问权限

前面写了一个拦截器,现在需要配置路由访问权限

(1)写一个JWT拦截器配置,写在config包里面,这一步真正实现了管理员和普通用户的授权

SecurityAndJwtConfig.java

kotlin 复制代码
package yellow.iblog.config;
​
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import yellow.iblog.jwt.JwtAuthenticationFilter;
​
@Configuration
public class SecurityAndJwtConfig {
​
    private final JwtAuthenticationFilter jwtFilter;
​
    public SecurityAndJwtConfig(JwtAuthenticationFilter jwtFilter) {
        this.jwtFilter = jwtFilter;
    }
​
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/auth/**").permitAll()   // 登录/注册接口放行
                        .requestMatchers("/admin/**").hasRole("ADMIN") // 只有管理员能访问
                        .anyRequest().authenticated()              // 其他都要认证,登陆了就可以访问
                )
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
//在这个UsernamePasswordAuthenticationFilter之前加入Filter
        return http.build();
    }
​
​
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}
​

.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)的作用是:

在springboot默认权限访问控制中间件UsernamePasswordAuthenticationFilter之前加入我们写的jwt中间件,

这个默认中间件就是读取请求里面的UserName和password做检查,如果正确,就登陆成功,然后发布token。这个就是有些杂糅的功能了。

加入了我们这个jwt中间件,默认中间件就会跳过了,因为在我们写的jwt中间件里面,我们已经创建了authentication,已经登陆成功了。

整个流程示意

less 复制代码
请求进入
    ↓
JWT 过滤器(您的自定义过滤器)
    ↓ ← 在这里设置 SecurityContextHolder 中的认证信息(设置了上下文,包括角色,UserName,uid)
UsernamePasswordAuthenticationFilter(Spring Security 的默认认证过滤器)
    ↓
FilterSecurityInterceptor(负责检查 authorizeHttpRequests 配置,就是.authorizeHttpRequests                          (auth -> auth
                        .requestMatchers("/auth/**").permitAll() 
                        .requestMatchers("/admin/**").hasRole("ADMIN") 
                        .anyRequest().authenticated()))
    ↓
跳转到相应的controller

7.如果需要更细粒度的访问控制,可以在controller方法上面使用注解

如果是上面已经配置过的路由,就不需要再写

less 复制代码
@PreAuthorize("isAuthenticated()")          // 只要认证(登录)即可,包括cookie登陆
@PreAuthorize("isAnonymous()")              // 必须是未认证用户(如登录页面)
@PreAuthorize("isFullyAuthenticated()")     // 完全认证(非remember-me登录)
@PreAuthorize("isRememberMe()")             // 通过remember-me认证(基于cookie登陆,下一次登陆的时候就不会要再次重新登录)
@PreAuthorize("hasRole('USER')")// 需要特定角色
@PreAuthorize("hasRole('ADMIN')")           // 必须有ADMIN角色
@PreAuthorize("hasAnyRole('ADMIN', 'USER')") // 有ADMIN或USER角色
@PreAuthorize("hasAuthority('READ_PRIVILEGE')") // 有特定权限
@PreAuthorize("hasAnyAuthority('READ', 'WRITE')") // 有READ或WRITE权限
// 检查principal的值(subject的值),如果subject是用户名,那么右边就要变成字符串,使用单引号
@PreAuthorize("authentication.name == 123456")
// 检查principal中的属性(假设principal有email属性)
@PreAuthorize("authentication.principal.email == 'admin@example.com'")
// 复杂的权限检查(如只能修改自己的数据)
@PutMapping("/user")
@PreAuthorize("#u.uid == authentication.name")
public ResponseEntity<ApiResponse<UserResponse>> updateUser(@RequestBody User u){
    User savedUser=userService.updateUser(u);
    if(savedUser!=null){
        UserResponse r=new UserResponse().FromUser(u);
        return ResponseEntity.ok(ApiResponse.success(r));
    } else{
        return ResponseEntity.badRequest().body(ApiResponse.fail("error"));
    }
}
// 多个条件组合
@PreAuthorize("hasRole('ADMIN') or hasRole('MODERATOR')")

8.可以在security那里配置permitAll,然后在contoller那里单独配置需要登陆验证的接口

scss 复制代码
  .requestMatchers("/article/**").permitAll()//默认放行

创作文章需要登陆

less 复制代码
​
    //用户写文章
    @PostMapping("")
    @PreAuthorize("isAuthenticated()")
    public ResponseEntity<ApiResponse<Article>> addArticle(@RequestBody Article article) {
        Article a=articleService.createArticle(article);
        if(a!=null){
            log.info("用户{}发布了一篇文章",a.getUid());
            return ResponseEntity.ok(ApiResponse.success(a));
        }
        //出错了一般不会返回这个,而是会被全局异常捕获器捕。
        // 只有业务出错但是代码没有错的时候才会返回
        return ResponseEntity.internalServerError().body(ApiResponse.fail("error"));
​
    }

9.使用上下文信息的方法

less 复制代码
 @DeleteMapping("/admin/article")
    public ResponseEntity<ApiResponse<Boolean>> adminDeleteArticleByAid(
            @RequestParam Long aid){
        // 从 SecurityContext 获取认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Long uid = Long.valueOf(authentication.getName()); // 获取用户ID
        if(articleService.deleteArticleByAid(aid)){

            log.info("管理员{}删除了文章{}",uid,aid);
            return ResponseEntity.ok(ApiResponse.success(true));
        }
        return ResponseEntity.internalServerError().body(ApiResponse.fail("error"));

    }
相关推荐
無限進步D5 小时前
Java 运行原理
java·开发语言·入门
難釋懷5 小时前
安装Canal
java
是苏浙5 小时前
JDK17新增特性
java·开发语言
不光头强5 小时前
spring cloud知识总结
后端·spring·spring cloud
GetcharZp8 小时前
告别 Python 依赖!用 LangChainGo 打造高性能大模型应用,Go 程序员必看!
后端
阿里加多8 小时前
第 4 章:Go 线程模型——GMP 深度解析
java·开发语言·后端·golang
likerhood9 小时前
java中`==`和`.equals()`区别
java·开发语言·python
小小李程序员9 小时前
Langchain4j工具调用获取不到ThreadLocal
java·后端·ai