前言
接着《初识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验证过滤器
注意:
在过滤器中无法初始化Bean组件
在过滤器中抛出的异常无法被全局异常处理器捕获
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),处理用户权限不足等。
版权归原作者 翰戈.summer 所有, 如有侵权,请联系我们删除。