目录
前言
数据库设计
用户表
角色表
用户角色表
权限表
角色权限表
插入数据
表的实体类
用户表实体类
角色表实体类
权限表实体类
mapper层接口
UserMapper
RoleMapper
AuthorityMapper
封装登录信息
统一响应结果
上下文相关类
jwt令牌工具类
依赖导入
属性类
yml配置文件
jwt令牌工具类
SpringContextUtils工具类
实现接口UserDetailsService
登录接口LoginController
自定义token验证过滤器
配置过滤器链
SecurityConfig完整配置
测试接口
开始测试
登录测试
权限测试
测试管理员权限访问
测试用户权限访问
总结
接着《初识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); // aOF9ij55dB7X2ffXby16Qu8n6Y96NV.RtHcza4vWO1EjoFO2JrsiW }
insert into tb_user (id, username, password) values (null, '艾伦', 'aOF9ij55dB7X2ffXby16Qu8n6Y96NV.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; }
import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper { UserEntity selectUserByUsername(String username); }
import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface RoleMapper { ListselectRoleByUsername(String username); }
import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface AuthorityMapper { List selectAuthorityByRole(String role); }
登录请求必须要提供用户名、密码和角色,后面都会用到。
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 Resultimplements Serializable { // 响应码:1代表成功,0代表失败 private Integer code; // 提示信息 private String message; // 响应数据 private T data; public static Result success() { Result result = new Result<>(); result.code = 1; return result; } public static Result success(T object) { Result result = new Result<>(); result.code = 1; result.data = object; return result; } public static Result error(String message) { Result result = new Result<>(); result.code = 0; result.message = message; return result; } }
ThreadLocal线程局部变量,将信息放入上下文,后面要用可以直接取出。
public class BaseContext { public static ThreadLocalthreadLocal = 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令牌依赖
io.jsonwebtoken jjwt0.9.1
配置处理器依赖
org.springframework.boot spring-boot-configuration-processortrue
以下依赖必须导入,否则jwt令牌用不了。
javax.xml.bind jaxb-api2.3.1 javax.activation activation1.1.1 org.glassfish.jaxb jaxb-runtime2.3.3
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; }
token: jwt: signing-key: jwt-token-signing-key #签名密钥 expire: 7200000 #有效时间
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(Mapclaims) { 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工具类用于在过滤器中获取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 staticT getBean(String name) throws BeansException { if (applicationContext == null) { return null; } return (T) applicationContext.getBean(name); } }
实现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(); Listroles = 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 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()) ); } }
将角色信息放入上下文中,在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 Resultlogin(@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 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()); } } }
注意:
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(); }
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),处理用户权限不足等。
上一篇:Java如何连接数据库