引用 项目集成Spring Security
Spring Security 一句话概述:一组 filter 过滤器链组成的权限认证。
HOPE
这里仅仅只记录下用法和代码,关于系统了解 Security 方面的内容并不会涉及,毕竟不是不了解而是根本不了解啊 >﹏< 也还指望着用着用着然后顺便 Ctrl+B 一下就慢慢了解了。
如果有有误的地方,希望多多指正哈ヾ(•ω•`)o
实际上教主为了把这个功能弄出来还是看了好多好多的博客和教程,但是仍然费了不少时间。主要原因基本可以概括为对概念的理解力太差。
如果只用 Security 登个录、拦截个 /hello-world
、放行个 Swagger 其实还算容易,一是不用太多配置,二是毕竟有太多大神总结的代码可参考。可是关于其它的用户可能就需要了解一下 Security 的过滤器链了。
后面基本上已经列出了所有的代码,这里还是把代码地址给出来:https://gitee.com/icefery/security-demo
申明
- 这里的权限和资源的概念相对与 RBAC0 来说相对比较模糊一点。(RBAC权限模型:RBAC权限模型——项目实战)
- 角色应该是权限的集合,权限应该是操作的集合,API 资源和页面路由资源应该再在权限的基础上建立关系。而在这里大致可以将角色理解为权限,API 理解为资源。而且鉴于很多地方都这样设计,似乎也无妨甚至更加简洁。
注意
使用 url 来进行权限验证后,意味着一些匿名可访问的资源可能就需要存如资源表了。
如配置 swagger 允许匿名访问,之前可以直接:
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("doc.html", "swagger-ui.html", "/webjars/**", "/swagger-resources/**", "/v2/**"); }
PS:虽然好像
web.ignore()
会绕开过滤器,但似乎这里还是会被拦截。(参考:springsecurity的http.permitall与web.ignoring的区别)
引用 Ant 风格路径表达式
通配符:
?
:匹配任何单字符*
:匹配 0 或者任意数量的字符**
:匹配 0 或者更多的目录
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.1.RELEASEversion>
<relativePath/>
parent>
<groupId>xyz.icefery.demo.securitygroupId>
<artifactId>security-demoartifactId>
<version>0.0.1-SNAPSHOTversion>
<properties>
<java.version>11java.version>
<mybatis-plus.version>3.3.2mybatis-plus.version>
<velocity.version>2.0velocity.version>
<jwt.version>0.9.0jwt.version>
properties>
<dependencies>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>${mybatis-plus.version}version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>${mybatis-plus.version}version>
dependency>
<dependency>
<groupId>org.apache.velocitygroupId>
<artifactId>velocity-engine-coreartifactId>
<version>${velocity.version}version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>${jwt.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
dependency>
<dependency>
<groupId>javax.xml.bindgroupId>
<artifactId>jaxb-apiartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
提示
pom.xml 中:
spring-boot-configuration-processor
:将配置文件注入到 Bean 中jaxb-api
:JDK11 缺少的 XML 序列化包
只有数据源的配置:
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/security_demo
spring.datasource.username = root
spring.datasource.password = root
由 Navicat 导出:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`anonymous` tinyint(1) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, '/hello1/**', 0);
INSERT INTO `sys_permission` VALUES (2, '/hello2', 0);
INSERT INTO `sys_permission` VALUES (3, '/hello3', 0);
INSERT INTO `sys_permission` VALUES (4, '/hello4', 1);
INSERT INTO `sys_permission` VALUES (5, '/doc.html', 1);
INSERT INTO `sys_permission` VALUES (6, '/swagger-ui.html', 1);
INSERT INTO `sys_permission` VALUES (7, '/webjars/**', 1);
INSERT INTO `sys_permission` VALUES (8, '/swagger-resources/**', 1);
INSERT INTO `sys_permission` VALUES (9, '/v2/**', 1);
INSERT INTO `sys_permission` VALUES (10, '/hello5', 0);
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'ROLE_1');
INSERT INTO `sys_role` VALUES (2, 'ROLE_2');
INSERT INTO `sys_role` VALUES (3, 'ROLE_3');
-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`role_id` bigint(0) NOT NULL,
`permission_id` bigint(0) NOT NULL,
PRIMARY KEY (`role_id`, `permission_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
INSERT INTO `sys_role_permission` VALUES (1, 1);
INSERT INTO `sys_role_permission` VALUES (2, 2);
INSERT INTO `sys_role_permission` VALUES (3, 3);
-- ----------------------------
-- Table structure for sys_role_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_user`;
CREATE TABLE `sys_role_user` (
`role_id` bigint(0) NOT NULL,
`user_id` bigint(0) NOT NULL,
PRIMARY KEY (`role_id`, `user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role_user
-- ----------------------------
INSERT INTO `sys_role_user` VALUES (1, 1);
INSERT INTO `sys_role_user` VALUES (3, 1);
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$dtpTb./Elr8HDFBP3Hk5EObrLJXjhZQh5AnZdP./cAtyF8TN3EPAC', 'admin');
INSERT INTO `sys_user` VALUES (2, 'user', '$2a$10$z.ySyainS7hrFgYKHCia4ePYgCAxhQ90K3ANtcQHJ6yoack/aHmfS', 'user');
SET FOREIGN_KEY_CHECKS = 1;
提示
例子中:
- 用户
admin
拥有角色ROLE_1
、ROLE_3
,密码为admin
- 用户
user
,密码为user
- 角色
ROLE_1
对应权限(资源)/hello1/**
- 角色
ROLE_2
对应权限(资源)/hello2
- 角色
ROLE_3
对应权限(资源)/hello3
用户插入:
- 可以在测试类中注入
PasswordEncoder
加密后插入记录- 在线计算器:Bcrypt密码生成计算器
@MapperScan("xyz.icefery.demo.security.mapper")
@EnableTransactionManagement
@Configuration
public class MyBatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}
提示
用到的只有
@MapperScan
注解,加在主类上亦可。
public class CodeGenerator {
public static void main(String[] args) {
String projectPath = System.getProperty("user.dir");
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig()
.setOutputDir(projectPath + "/src/main/java/")
.setFileOverride(false)
.setOpen(false)
.setAuthor("icefery")
.setIdType(IdType.AUTO)
.setServiceName("%sService");
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig()
.setDbType(DbType.MYSQL)
.setDriverName("com.mysql.cj.jdbc.Driver")
.setUrl("jdbc:mysql://localhost:3306/security_demo")
.setUsername("root")
.setPassword("root");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig()
.setParent("xyz.icefery.demo.security");
mpg.setPackageInfo(pc);
// 策略配置
StrategyConfig sg = new StrategyConfig()
.setTablePrefix("sys_")
.setInclude("sys_user",
"sys_role",
"sys_permission",
"sys_role_user",
"sys_role_permission")
.setNaming(NamingStrategy.underline_to_camel)
.setColumnNaming(NamingStrategy.underline_to_camel)
.setRestControllerStyle(true)
.setControllerMappingHyphenStyle(true)
.setChainModel(true)
.setEntityLombokModel(true);
mpg.setStrategy(sg);
// 执行
mpg.execute();
}
}
申明
生成代码时的全局配置的主键策略为自增,而 MP 不支持复合主键。于是把中间表的主键注解去掉(默认为
IdType.NONE
)
查询用户的角色列表:(在告知 Security 用户的角色列表时会用到)
查询访问资源需要的角色列表:(在匹配 url 后查询访问资源需要的角色列表时会用到)
jwt.properties:
jwt.secret = secret
# 一天:1000*60*60*24
#jwt.expiration = 86400000
# 两分钟:1000*60*2
jwt.expiration = 120000
jwt.token-header = Authorization
@Data
@Component
@PropertySource("classpath:jwt.properties")
@ConfigurationProperties(prefix = "jwt")
public class JwtConfig {
// 密钥
private String secret;
// 过期时间
private Long expiration;
// 请求头
private String tokenHeader;
// 生成 Token
public String createToken(String username) {
long now = System.currentTimeMillis();
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date(now))
.setExpiration(new Date(now + expiration))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
// 校验 Token
public Boolean validateToken(String token, String username) {
Claims claims = this.extractClaims(token);
boolean isUsernameCorrect = claims.getSubject().equals(username);
boolean isTokenExpired = claims.getExpiration().before(new Date());
return isUsernameCorrect && !isTokenExpired;
}
public Claims extractClaims(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
// 从 Token 中提取用户名
public String extractUsername(String token) {
return this.extractClaims(token).getSubject();
}
}
public class SecurityUserDetails implements UserDetails {
private final String username;
private final String password;
private final List<GrantedAuthority> grantedAuthorityList;
public SecurityUserDetails(User user, List<Role> roleList) {
this.username = user.getUsername();
this.password = user.getPassword();
this.grantedAuthorityList = roleList
.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.grantedAuthorityList;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
用户登录后将用户的信息(用户名、密码、角色列表)加载进 UserDetails
(实现类SecurityUserDetails
)中。
@Slf4j
@Component
public class SecurityUserDetailsService implements UserDetailsService {
@Autowired private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User one = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
if (one == null) {
log.info("用户[" + username + "]不存在");
throw new UsernameNotFoundException("用户[" + username + "]不存在");
}
List<Role> roleList = userService.getRoleListById(one.getId());
log.info("用户[" + username + "]的角色有" + roleList.stream().map(Role::getName).collect(Collectors.toList()));
return new SecurityUserDetails(one, roleList);
}
}
此过滤器会在 Security 的配置中放在 UsernamePasswordAuthenticationFilter 之前。
// Token 过滤器
@Slf4j
@Component
public class JwtAntuenticationFilter extends OncePerRequestFilter {
@Autowired private JwtConfig jwtConfig;
@Autowired private SecurityUserDetailsService securityUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 从请求头中拿到 token
String token = request.getHeader(jwtConfig.getTokenHeader());
if (token != null && !token.isBlank()) {
String username = null;
try {
username = jwtConfig.extractUsername(token);
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
log.info("Token 过期或有误");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 检验 token
boolean valid = jwtConfig.validateToken(token, username);
if (valid) {
// 加载登录信息(用户名 | 密码 | 角色)
UserDetails userDetails = securityUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
@Slf4j
@Component
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired private AntPathMatcher antPathMatcher;
@Autowired private PermissionService permissionService;
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
log.info("请求 [" + requestUrl + "]");
// 匹配 url | 查找角色
List<Permission> permissionList = permissionService.list();
// TODO: 排序 url 使其能优先比较更细粒度的 url
for (Permission p : permissionList) {
if (antPathMatcher.match(p.getUrl(), requestUrl)) {
if (p.getAnonymous()) {
log.info("[" + requestUrl + "] 允许匿名访问");
// 返回 null 不会进入 UrlAccessDecisionManager#decide 方法 | 直接放行
// 如果 authentication instanceof AnonymousAuthenticationToken | 即"用户"是匿名访问时用户拥有的角色
// 那么"用户"拥有的角色集合 authentication.getAuthorities() 里的角色为 ROLE_ANONYMOUS
// 因此也可返回资源需要匿名访问的角色为 ROLE_ANONYMOUS | return SecurityConfig.createList("ROLE_ANONYMOUS")
// 然后在 UrlAccessDecisionManager#decide 方法里进行判断 | 如果 "ROLE_ANONYMOUS".equals(needRoleName) 则 return 放行
return null;
}
List<Role> roleList = permissionService.getRoleListById(p.getId());
// ROLE_LOGIN 是自定义的需要登录访问的角色
if (roleList.isEmpty()) {
log.info("资源 [" + p.getUrl() + "] 无需角色 | 登录即可访问");
return SecurityConfig.createList("ROLE_LOGIN");
}
String[] roleNames = roleList.stream().map(Role::getName).toArray(String[]::new);
log.info("资源 [" + p.getUrl() + "] 需要角色 " + Arrays.toString(roleNames));
return SecurityConfig.createList(roleNames);
}
}
log.info("请求 [" + requestUrl + "] 未在资源表中, 登录即可访问");
// ROLE_LOGIN 是自定义的需要登录访问的角色
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return FilterInvocation.class.isAssignableFrom(aClass);
}
@Bean
public AntPathMatcher antPathMatcher() {
return new AntPathMatcher();
}
}
提示
有些博文里是用一个 Map 在构造方法中将 url 映射信息先加载出来,而这里希望的是每次请求到来时都去数据库读取最新的映射信息。
策略概要:
- 匿名访问:用
anonymous
来标记是否允许匿名访问- 需要角色的资源,用户只需具备其中一个角色即可
- 不需要角色的资源以及未在资源表中的资源,默认登录即可访问
Security 的角色名需要以
ROLE_
开头。而如果用户未通过 Security 验证,在AccessDecisionManager
中的用户角色默认为ROLE_ANONYMOUS
@Slf4j
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : collection) {
String needRoleName = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(needRoleName)) {
if (authentication instanceof AnonymousAuthenticationToken) {
log.info("资源需要登录访问 | 当前未登录");
throw new InsufficientAuthenticationException("未登录");
} else if (authentication instanceof UsernamePasswordAuthenticationToken) {
log.info("资源需要登录访问 | 当前已登录 | 正常访问");
return;
}
}
for (GrantedAuthority authority : authentication.getAuthorities()) {
log.info("资源需要角色 [" + needRoleName + "] 用户拥有角色 [" + authority.getAuthority() + "]");
if (authority.getAuthority().equals(needRoleName)) {
return;
}
}
}
log.info("当前登录用户无访问资源所需角色");
throw new AccessDeniedException("权限不足");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
@Slf4j
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private JwtConfig jwtConfig;
@Autowired private JwtAntuenticationFilter jwtAntuenticationFilter;
@Autowired private SecurityUserDetailsService securityUserDetailsService;
@Autowired private UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;
@Autowired private UrlAccessDecisionManager urlAccessDecisionManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭 csrf
http.csrf().disable();
// 开启跨域
http.cors();
http.authorizeRequests()
// 使用 URL 权限验证
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource);
object.setAccessDecisionManager(urlAccessDecisionManager);
return object;
}
})
.anyRequest().authenticated();
// 表单登录
http.formLogin()
.loginProcessingUrl("/login")
// 登录成功
.successHandler((request, response, authentication) -> {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtConfig.createToken(userDetails.getUsername());
response.setHeader(jwtConfig.getTokenHeader(), token);
response.setCharacterEncoding("UTF-8");
response.setContentType("text/plain;charset=utf-8");
response.getWriter().write("登录成功");
})
// 登录失败
.failureHandler((request, response, authenticationException) -> {
String reason;
if (authenticationException instanceof UsernameNotFoundException || authenticationException instanceof BadCredentialsException) {
reason = "用户名不存在或密码错误";
} else if (authenticationException instanceof AccountExpiredException) {
reason = "账号过期";
} else if (authenticationException instanceof CredentialsExpiredException) {
reason = "密码过期";
} else if (authenticationException instanceof LockedException) {
reason = "账号锁定";
} else if (authenticationException instanceof DisabledException) {
reason = "账号禁用";
} else {
reason = "其它原因";
}
response.setCharacterEncoding("UTF-8");
response.setContentType("text/plain;charset=utf-8");
response.getWriter().write(reason);
});
http.exceptionHandling()
// 登录过期 | 未登录
.authenticationEntryPoint((request, response, authenticationException) -> {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/plain;charset=utf-8");
response.getWriter().write("登录过期 | 未登录");
})
// 权限不足
.accessDeniedHandler((request, response, accessdeniedException) -> {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/plain;charset=utf-8");
response.getWriter().write("权限不足");
});
// JWT 过滤器
http.addFilterBefore(jwtAntuenticationFilter, UsernamePasswordAuthenticationFilter.class);
// 禁用 session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(securityUserDetailsService).passwordEncoder(passwordEncoder());
}
}
注意
跨域问题:在Security 中,除了使用如下的方式配置 CORS 配置:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedHeaders("*") .allowedOrigins("*") .allowCredentials(true) .allowedMethods("*") .maxAge(3600); } }
还需要在 SecurityConfig 中开启 CORS:
http.cors();
session 问题:使用 JWT 进行验证,如果不禁用 session 可能会出现一些问题。
提示
- Security 用户名不存在时默认抛出的是
BadCredentialsException
并不会抛出UsernameNotFoundException
。(解决方案参考:Springboot + SpringSecurity 中不能抛出异常UserNameNotFoundException 解析)- 登录失败的其它异常可以根据需要抛出,在 SecurityUserDetails 有对应的属性。
- 当用户验证失败或匿名访问时,登录过期或者未登录处理器中
authenticationException
异常的实际类型是InsufficientAuthenticationException
类型
@RestController
public class HelloController {
// 在权限表中 | 访问资源需要角色 ROLE_1
@GetMapping("/hello1")
public String hello1() {
return "hello1";
}
// 在权限表中 | 访问资源需要角色 ROLE_2
@GetMapping("/hello2")
public String hello2() {
return "hello2";
}
// 在权限表中 | 访问资源需要角色 ROLE_3
@GetMapping("/hello3")
public String hello3() {
return "hello3";
}
// 在权限表中 | 允许匿名访问
@GetMapping("/hello4")
public String hello4() {
return "hello4";
}
// 不在权限表中 | 匹配 /hello1/** | 访问资源需要 ROLE_1
@GetMapping("/hello1/sub")
public String hello1Sub() {
return "hello1_sub";
}
// 不在权限表中 | 不匹配 /hello3 | 访问资源需要登录
@GetMapping("/hello3/sub")
public String hello3Sub() {
return "hello3_sub";
}
// 在权限表中 | 访问资源不需要角色 | 访问资源需要登录
@GetMapping("/hello5")
public String hello5() {
return "Hello5";
}
}
/hello4
user
登录:只能访问 /hello4
、/hello3/sub
、/hello5
admin
登录:不能访问/hello2
申明