0


整合SpringSecurity

前言

接着《初识SpringSecurity》来看如何在项目中整合SpringSecurity这个安全框架。

https://blog.csdn.net/qq_74312711/article/details/134978245?spm=1001.2014.3001.5501

上次我们是将用户添加到内存中,实际开发中肯定是要存储在数据库里的。先来看数据库是如何设计的,以及如何把用户信息交给Security处理。

数据库设计

用户表

存放用户信息,主要是存放用户名和密码。

create table if not exists tb_user
(
    id       bigint auto_increment comment '主键'
        primary key,
    username varchar(16) not null comment '用户名',
    password varchar(64) not null comment '密码',
    constraint username
        unique (username)
)
    comment '用户表';

角色表

存放角色信息,角色是用户的身份。

create table if not exists tb_role
(
    id   bigint auto_increment comment '主键'
        primary key,
    role varchar(8) not null comment '角色',
    constraint role
        unique (role)
)
    comment '角色表';

用户角色表

用户和角色的关系:一个用户可以有多个角色身份,一个角色身份可以有多个用户对应。

create table if not exists tb_user_role
(
    id       bigint auto_increment comment '主键'
        primary key,
    username varchar(16) not null comment '用户名;不唯一',
    role     varchar(8)  not null comment '角色;不唯一'
)
    comment '用户角色表';

权限表

存放权限信息,角色拥有权限。

create table if not exists tb_authority
(
    id        bigint auto_increment comment '主键'
        primary key,
    authority varchar(16) not null comment '权限',
    constraint authority
        unique (authority)
)
    comment '权限表';

角色权限表

角色和权限的关系:一个角色可以拥有多个权限,一个权限可以被多个角色拥有。

create table if not exists tb_role_authority
(
    id        bigint auto_increment comment '主键'
        primary key,
    role      varchar(8)  not null comment '角色;不唯一',
    authority varchar(16) not null comment '权限;不唯一'
)
    comment '角色权限表';

插入数据

注意密码不能明文存储,要先经过编码处理。

@Test
void getEncodePassword() {
    String password = "123abc";
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    String encode = bCryptPasswordEncoder.encode(password);
    System.out.println(encode); // $2a$10$3OF9ij55dB7X2ffXby16Qu8n6Y96NV.RtHcza4vWO1EjoFO2JrsiW
}
insert into tb_user (id, username, password)
values (null, '艾伦', '$2a$10$3OF9ij55dB7X2ffXby16Qu8n6Y96NV.RtHcza4vWO1EjoFO2JrsiW');

insert into tb_role (id, role)
values (null, '管理员'),
       (null, '用户');

insert into tb_user_role (id, username, role)
values (null, '艾伦', '管理员'),
       (null, '艾伦', '用户');

insert into tb_authority (id, authority)
values (null, '权限1'),
       (null, '权限2'),
       (null, '权限3');

insert into tb_role_authority (id, role, authority)
values (null, '管理员', '权限1'),
       (null, '管理员', '权限2'),
       (null, '管理员', '权限3'),
       (null, '用户', '权限1'),
       (null, '用户', '权限2');

表的实体类

用户表实体类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@Data
@NoArgsConstructor
public class UserEntity {
    private Long id;
    private String username;
    private String password;
}

角色表实体类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@Data
@NoArgsConstructor
public class RoleEntity {
    private Long id;
    private String role;
}

权限表实体类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@Data
@NoArgsConstructor
public class AuthorityEntity {
    private Long id;
    private String authority;
}

mapper层接口

UserMapper

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
    UserEntity selectUserByUsername(String username);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.mapper.UserMapper">
    <!--查询用户-->
    <select id="selectUserByUsername" resultType="UserEntity">
        select id, username, password
        from tb_user
        where username = #{username};
    </select>
</mapper>

RoleMapper

import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface RoleMapper {
    List<RoleEntity> selectRoleByUsername(String username);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.mapper.RoleMapper">
    <!--查询角色-->
    <select id="selectRoleByUsername" resultType="RoleEntity">
        select tr.id, tr.role
        from tb_user_role tur
                 left join tb_role tr on tur.role = tr.role
        where tur.username = #{username};
    </select>
</mapper>

AuthorityMapper

import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface AuthorityMapper {
    List<AuthorityEntity> selectAuthorityByRole(String role);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.mapper.AuthorityMapper">
    <!--查询权限-->
    <select id="selectAuthorityByRole" resultType="AuthorityEntity">
        select ta.id, ta.authority
        from tb_role_authority tra
                 left join tb_authority ta on tra.authority = ta.authority
        where tra.role = #{role};
    </select>
</mapper>

封装登录信息

登录请求必须要提供用户名、密码和角色,后面都会用到。

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@Data
@NoArgsConstructor
public class UserLogin {
    private String username;
    private String password;
    private String role;
}

统一响应结果

import lombok.Data;

import java.io.Serializable;

@Data
public class Result<T> implements Serializable {

    // 响应码:1代表成功,0代表失败
    private Integer code;
    // 提示信息
    private String message;
    // 响应数据
    private T data;

    public static <T> Result<T> success() {
        Result<T> result = new Result<>();
        result.code = 1;
        return result;
    }

    public static <T> Result<T> success(T object) {
        Result<T> result = new Result<>();
        result.code = 1;
        result.data = object;
        return result;
    }

    public static <T> Result<T> error(String message) {
        Result<T> result = new Result<>();
        result.code = 0;
        result.message = message;
        return result;
    }
}

上下文相关类

ThreadLocal线程局部变量,将信息放入上下文,后面要用可以直接取出。

public class BaseContext {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void setContext(String context) {
        threadLocal.set(context);
    }

    public static String getContext() {
        return threadLocal.get();
    }

    public static void removeContext() {
        threadLocal.remove();
    }
}

jwt令牌工具类

依赖导入

jwt令牌依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

配置处理器依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

以下依赖必须导入,否则jwt令牌用不了。

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.3</version>
</dependency>

属性类

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "token.jwt")
public class JwtTokenProperties {
    // 签名密钥
    private String signingKey;

    // 有效时间
    private Long expire;
}

yml配置文件

token:
  jwt:
    signing-key: jwt-token-signing-key #签名密钥
    expire: 7200000 #有效时间

jwt令牌工具类

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Map;

@Component
@RequiredArgsConstructor
public class JwtTokenUtils {

    private final JwtTokenProperties jwtTokenProperties;

    public String getJwtToken(Map<String, Object> claims) {

        String signingKey = jwtTokenProperties.getSigningKey();
        Long expire = jwtTokenProperties.getExpire();

        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS256, signingKey)
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
    }

    public Claims parseJwtToken(String jwtToken) {

        String signingKey = jwtTokenProperties.getSigningKey();

        return Jwts.parser()
                .setSigningKey(signingKey)
                .parseClaimsJws(jwtToken)
                .getBody();
    }
}

SpringContextUtils工具类

SpringContextUtils工具类用于在过滤器中获取Bean,因为在过滤器中无法初始化Bean组件,所以使用上下文获取。

import jakarta.annotation.Nonnull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringContextUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    @Override
    public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }

    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException {
        if (applicationContext == null) {
            return null;
        }
        return (T) applicationContext.getBean(name);
    }
}

实现接口UserDetailsService

实现UserDetailsService接口,重写loadUserByUsername方法。方法返回一个User对象,即为UserDetails对象。我们将用户的信息封装到User对象中,返回给Security处理。

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.StringJoiner;

@Service
@RequiredArgsConstructor
public class UserLoginService implements UserDetailsService {

    private final UserMapper userMapper;
    private final RoleMapper roleMapper;
    private final AuthorityMapper authorityMapper;

    @Override
    public UserDetails loadUserByUsername(String username) {

        // 查询用户
        UserEntity userEntity = userMapper.selectUserByUsername(username);
        if (userEntity == null) {
            throw new RuntimeException("用户不存在");
        }

        // 获取用户登录身份role
        String role = BaseContext.getContext();

        List<RoleEntity> roles = roleMapper.selectRoleByUsername(username);
        // 判断用户是否有role身份
        boolean flag = true;
        for (RoleEntity r : roles) {
            if (r.getRole().equals(role)) {
                flag = false;
                break;
            }
        }
        if (flag) {
            throw new RuntimeException("用户" + username + "没有" + role + "身份");
        }

        // 查询角色权限
        List<AuthorityEntity> authorities = authorityMapper.selectAuthorityByRole(role);
        // 权限之间用","分隔
        StringJoiner stringJoiner = new StringJoiner(",", "", "");
        authorities.forEach(authority -> stringJoiner.add(authority.getAuthority()));

        return new User(userEntity.getUsername(), userEntity.getPassword(),
                AuthorityUtils.commaSeparatedStringToAuthorityList(stringJoiner.toString())
        );
    }
}

登录接口LoginController

将角色信息放入上下文中,在UserLoginService中会用到角色信息。

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;

@RestController
@RequiredArgsConstructor
public class LoginController {

    private final UserLoginService userLoginService;
    private final JwtTokenUtils jwtTokenUtils;

    @PostMapping("/login")
    public Result<String> login(@RequestBody UserLogin userLogin) {
        try {

            // 将登录用户角色放入上下文
            BaseContext.setContext(userLogin.getRole());

            UserDetails userDetails = userLoginService.loadUserByUsername(userLogin.getUsername());
            // 获取用户权限
            StringJoiner authorityString = new StringJoiner(",", "", "");
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                authorityString.add(authority.getAuthority());
            }

            Map<String, Object> claims = new HashMap<>();
            claims.put("username", userLogin.getUsername());
            claims.put("role", userLogin.getRole());
            claims.put("authorityString", authorityString.toString());
            String jwtToken = jwtTokenUtils.getJwtToken(claims);

            return Result.success(jwtToken);
        } catch (Exception e) {
            return Result.error(e.getMessage());
        }
    }
}

自定义token验证过滤器

注意:

  1. 在过滤器中无法初始化Bean组件

  2. 在过滤器中抛出的异常无法被全局异常处理器捕获

import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException {
        try {
            // 获取请求路径
            String url = request.getRequestURL().toString();

            // 为登录请求放行
            if (url.contains("/login")) {
                filterChain.doFilter(request, response);
                return; // 结束方法
            }

            // 获取请求头中的token
            String jwtToken = request.getHeader("token");
            if (!StringUtils.hasLength(jwtToken)) {
                // token不存在,交给其他过滤器处理
                filterChain.doFilter(request, response);
                return; // 结束方法
            }

            // 过滤器中无法初始化Bean组件,使用上下文获取
            JwtTokenUtils jwtTokenUtils = SpringContextUtils.getBean("jwtTokenUtils");
            if (jwtTokenUtils == null) {
                throw new RuntimeException();
            }

            // 解析jwt令牌
            Claims claims;
            try {
                claims = jwtTokenUtils.parseJwtToken(jwtToken);
            } catch (Exception e) {
                throw new RuntimeException();
            }

            // 获取用户信息
            String username = (String) claims.get("username"); // 用户名
            String authorityString = (String) claims.get("authorityString"); // 权限

            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    username, null,
                    AuthorityUtils.commaSeparatedStringToAuthorityList(authorityString)
            );

            // 将用户信息放入SecurityContext上下文
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // 将用户名放入线程局部变量
            BaseContext.setContext(username);

            filterChain.doFilter(request, response);
        } catch (Exception e) {
            // 过滤器中抛出的异常无法被全局异常处理器捕获,直接返回错误结果
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/json; charset=utf-8");
            String value = new ObjectMapper().writeValueAsString(Result.error("token验证失败"));
            response.getWriter().write(value);
        }
    }
}

配置过滤器链

注意给登录请求放行,要不然访问不了登录接口。

// 配置过滤器链
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
    httpSecurity.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
            .requestMatchers(HttpMethod.POST, "/login").permitAll() // 登录请求放行
            .requestMatchers(HttpMethod.GET, "/test1").hasAnyAuthority("权限1", "权限2")
            .requestMatchers(HttpMethod.GET, "/test2").hasAuthority("权限3")
    );
    httpSecurity.authenticationProvider(authenticationProvider());

    // 禁用登录页面
    httpSecurity.formLogin(AbstractHttpConfigurer::disable);
    // 禁用登出页面
    httpSecurity.logout(AbstractHttpConfigurer::disable);
    // 禁用session
    httpSecurity.sessionManagement(AbstractHttpConfigurer::disable);
    // 禁用httpBasic
    httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
    // 禁用csrf保护
    httpSecurity.csrf(AbstractHttpConfigurer::disable);

    // 通过上下文获取AuthenticationManager
    AuthenticationManager authenticationManager = SpringContextUtils.getBean("authenticationManager");
    // 添加自定义token验证过滤器
    httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);

    return httpSecurity.build();
}

SecurityConfig完整配置

import lombok.RequiredArgsConstructor;
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.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final UserDetailsService userDetailsService;

    // 加载用户信息
    @Bean
    public UserDetailsService userDetailsService() {
        return userDetailsService;
    }

    // 身份验证管理器
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    // 处理身份验证
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        return daoAuthenticationProvider;
    }

    // 密码编码器
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 配置过滤器链
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                .requestMatchers(HttpMethod.POST, "/login").permitAll() // 登录请求放行
                .requestMatchers(HttpMethod.GET, "/test1").hasAnyAuthority("权限1", "权限2")
                .requestMatchers(HttpMethod.GET, "/test2").hasAuthority("权限3")
        );
        httpSecurity.authenticationProvider(authenticationProvider());

        // 禁用登录页面
        httpSecurity.formLogin(AbstractHttpConfigurer::disable);
        // 禁用登出页面
        httpSecurity.logout(AbstractHttpConfigurer::disable);
        // 禁用session
        httpSecurity.sessionManagement(AbstractHttpConfigurer::disable);
        // 禁用httpBasic
        httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
        // 禁用csrf保护
        httpSecurity.csrf(AbstractHttpConfigurer::disable);

        // 通过上下文获取AuthenticationManager
        AuthenticationManager authenticationManager = SpringContextUtils.getBean("authenticationManager");
        // 添加自定义token验证过滤器
        httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }
}

测试接口

用户有权限访问/test1,没有权限访问/test2。管理员有权限访问/test1和/test2。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @GetMapping("/test1")
    public String demo1() {
        System.out.println("test1访问成功!");
        return "test1访问成功!";
    }

    @GetMapping("/test2")
    public String demo2() {
        System.out.println("test2访问成功!");
        return "test2访问成功!";
    }
}

开始测试

登录测试

测试成功

权限测试

测试管理员权限访问

/test1访问成功

/test2测试成功

测试用户权限访问

用户登录

/test1访问成功

/test2访问失败

可以看到测试的结果都是正确的,说明成功地实现了权限控制。

总结

以上就是如何在项目中整合SpringSecurity的基本用法,我们再来看一下官方的描述:

Spring Security是一个强大且高度可定制的身份验证和访问控制框架。它是保护基于Spring的应用程序的事实标准。

Spring Security是一个专注于为Java应用程序提供身份验证和授权的框架。与所有Spring项目一样,Spring Security的真正力量在于它可以多么容易地扩展以满足自定义需求。

强大且高度可定制就是SpringSecurity受欢迎的关键,我们还可以对以上的案例进行优化。例如,我们不将用户角色的权限放在token令牌中,而是放在Redis中。在进行token验证的时候,解析出用户名,拿用户名去Redis中找对应的权限。又或者我们可以自定义处理器,处理用户未登录(未携带token),处理用户权限不足等。


本文转载自: https://blog.csdn.net/qq_74312711/article/details/134992609
版权归原作者 翰戈.summer 所有, 如有侵权,请联系我们删除。

“整合SpringSecurity”的评论:

还没有评论