环境
springboot:1.5
Intellij IDEA:2021.1
序言
以前只是用到架构师搭好的环境,具体怎么配置,怎么用并不是很清楚;
最近因为项目的原因,研究了下,虽然最终没有用上,但是研究的成果,我得记录下来;
步骤
这里假设已经是springboot项目
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
放开URL
登录这块,公司有统一的SSO,并不需要用到spring security
的登录控制。
所以我需要对所有的URL进行开放。只想控制方法层。
创建配置类WebSecurityConfig
package com.sgy.securitydemo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
//开启Spring Security的功能
@EnableWebSecurity
//添加@EnableGlobalMethodSecurity注解开启Spring方法级安全
//prePostEnabled属性决定Spring Security的前注解是否可用@PreAuthorize,@PostAuthorize等注解,设置为true
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyPermissionEvaluator myPermissionEvaluator;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/*
* 在内存中配置两个用户 admin和user
*/
auth.inMemoryAuthentication()
.withUser("admin")
.password(passwordEncoder().encode("123456"))
.roles("admin");
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder().encode("123456"))
.roles("user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 定义哪些URL需要被保护、哪些不需要被保护
http.authorizeRequests()
// 设置所有人都可以访问登录页面
.antMatchers("/**").permitAll();
// 任何请求,登录后可以访问
// .anyRequest().authenticated()
// .and()
// .formLogin().loginPage("/");
http.authorizeRequests().expressionHandler(defaultWebSecurityExpressionHandler());
}
/**
* 为了将自定义的myPermissionEvaluator注入给spring security
* @return
*/
@Bean
public DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler() {
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setPermissionEvaluator(myPermissionEvaluator);
return handler;
}
/**
* 解决跨域问题
* @return
*/
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.addAllowedOrigin(CorsConfiguration.ALL);
configuration.addAllowedHeader(CorsConfiguration.ALL);
configuration.addAllowedMethod(CorsConfiguration.ALL);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
/*
* 指定加密方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
使用@PreAuthorize进行方法控制
@RestController
public class HelloController {
@GetMapping("/helloAdmin")
@PreAuthorize("hasPermission('', 'user:view')")
public String helloAdmin() {
return "I am Admin";
}
}
通过查看源码我们知道:
public boolean hasPermission(Object target, Object permission) {
return this.permissionEvaluator.hasPermission(this.authentication, target, permission);
}
因为没有使用spring security
的登录功能,所以this.authentication
肯定就是匿名用户,后面两个:target
和permission
参数,就是我们在使用@PreAuthorize("hasPermission('', 'user:view')")
注解时,传的两个字符串。target我们传''
,permission我们传的是user:view
(自己定义的)。
接着看源码我们发现permissionEvaluator.hasPermission()
方法有个默认实现:
public boolean hasPermission(Authentication authentication, Object target, Object permission) {
this.logger.warn(LogMessage.format("Denying user %s permission '%s' on object %s", authentication.getName(), permission, target));
return false;
}
只是打印了下日志,并且始终返回false
。
这也就意味着我们需要重写hasPermission()
方法。
重写hasPermission()方法
重写hasPermission()方法方法需要实现PermissionEvaluator
接口。
package com.sgy.securitydemo.config;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
@Component
public class MyPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object o, Object o1) {
//假设是HTTP请求,那么用户ID问问需要到HttpServletRequest中去获取。
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
System.out.println(o + "-=-=-=-=" + o1 + authentication);
//这里写业务判断来决定是否返回true or false
return true;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable serializable, String s, Object o) {
return false;
}
}
实际业务开发:我们先去用户权限表中查询该用户权限,然后比较该用户是否拥有该方法的权限。
方法的权限就是我们配置@PreAuthorize("hasPermission('', 'user:view')")
中的user:view
。
全局异常
当权限不通过时,我们往往会抛出业务异常。
这时我们还需要一个全局异常捕获:
package com.sgy.securitydemo.config;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionRespone {
/**
* 全局异常捕获
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
public Object errorHandler( Exception e) {
//这里写的很简单,实际上应该转成业务对象返回出去。
e.printStackTrace();
return null;
}
//具体业务写法参考:
/**
* 捕获所有异常
*/
@ExceptionHandler(Throwable.class)
@ResponseBody
public ResponseEntity<?> handleAllException(HttpServletRequest request,
HttpServletResponse response,
Throwable error) {
JsonResult json = exceptionHandler.handleException(request, response, error);
//401 error
if (error instanceof AccessDeniedException) {
json.setErrMessage(Optional.of("您没有权限,请联系管理员"));
json.setErrCode(Optional.of(SC_UNAUTHORIZED));
response.setStatus(SC_UNAUTHORIZED);
}
ResponseEntity.BodyBuilder resp = ResponseEntity.status(response.getStatus());
if (_isJsonRequest(request)) {
return resp.body(json);
} else {
// 如有必要,可以 Forward 到一个错误页面;或者定义一个 ErrorController 和 error 页面。
String errMsg = String.format("出错啦:%s", json.getErrMessage().orElse("未知错误!"));
return resp.body(errMsg);
}
}
}
总结
①方法级控制,本质上也是AOP控制,在方法执行前进行AOP处理。
只不过spring security帮我们做了很多。比如提供@PreAuthorize
注解和hasPermission()
方法等等。
②重点是重写hasPermission()方法这一步,里面包含的是我们的业务。
直接参考地址
pringBoot整合Spring Security安全框架(一)
spingsecurity中haspermission用法以及配置自定义PermissionEvaluator