@File
2019-10-19T07:28:07.000000Z
字数 20162
阅读 89
java
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- social -->
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-web</artifactId>
<version>1.1.6.RELEASE</version>
</dependency>
<!-- jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
spring:
datasource:
url: jdbc:mysql://47.107.167.205:8888/test?useSSL=false&serverTimezone=UTC
username: root
password:
driver-class-name: com.mysql.cj.jdbc.Driver
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private DataSource dataSource;
/**
* 查询逻辑
*/
@Resource
private UserSecurityService userSecurityService;
/**
* 验证码过滤类
*/
@Resource
private ImageCodeValidateFilter imageCodeValidateFilter;
/**
* 继承 `WebSecurityConfigurerAdapter` 并重写 `configure` 方法
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 请求前执行过滤(四、7. 验证码过滤)
http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)
// ====== 基础登录设置 ====== //
.formLogin() // 采用表单登录
.loginPage("/login.html") // 登录页面
.loginProcessingUrl("/authentication/form") // 请求接口(不用实现,自动调用 四、1.登录)
// 成功逻辑(可调用 四、3.登录成功类)
.successHandler((req,resp,exception) -> {
resp.setContentType("applicatoin/json;charset=utf-8");
resp.getWriter().write("登录成功");
})
// 失败逻辑(可调用 四、4.登录失败类)
.failureHandler((req,resp,execption) -> {
resp.setContentType("applicatoin/json;charset=utf-8");
resp.getWriter().write("登录失败");
})
// ====== 退出登录 ====== //
.and()
.logout() // 开启访问 /logout 实现功能
.logoutSuccessUrl("/login.html") // 退出后跳转的页面
// ====== 权限认证(可选) ====== //
.and() // 功能分隔,表示进行其他的配置
.authorizeRequests() // 表示所有的都需要认证
.antMatchers("/login.html", "/js/**", "/css/**") // 白名单
.permitAll()
.anyRequest() // 对于所有的请求
.authenticated() // 认证后才能访问
// ====== 记住登录状态(可选) ====== //
// 前端传参 remember-me:true|false
.and()
.rememberMe() // 功能开启
.tokenValiditySeconds(360000) // 记录时长
.tokenRepository(persistentTokenRepository()) // 指定token库
.userDetailsService(userSecurityService)
// .alwaysRemember(true) // true 时只能记住
// ====== 关闭跨站请求伪造功能(必须) ====== //
.and()
.csrf().disable();
}
/**
* 配置密码加密规则
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置token验证
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository
= new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// 是否自动建表
jdbcTokenRepository.setCreateTableOnStartup(false);
return jdbcTokenRepository;
}
}
UserDetailsService
接口
@Component
public class UserSecurityService implements UserDetailsService {
/**
* service 层
*/
@Resource
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
// 通过用户名查出数据(三、2.通过用户名获取数据)
SysUser user = sysUserService.getUser(name);
// 通过查询结果的密码做比对
User admin = new User(
// 登录名
name,
// 用户密码
user.getPassword(),
// 权限(用于 八、权限验证),必须 ROLE_ 前缀
Arrays.asList(new SimpleGrantedAuthority("ROLE_admin")
));
return admin;
}
}
@Service
public class SysUserService {
@Resource
private JdbcTemplate jdbcTemplate;
public SysUser getUser(String name) {
String sql = "SELECT * FROM `sys_user` WHERE `username` = ?";
SysUser sysUser = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(SysUser.class), name);
return sysUser;
}
}
AuthenticationSuccessHandler
接口
@Component
public class MySuccessAuthenticationHandler implements AuthenticationSuccessHandler {
/**
* 登陆成功
* @param request 请求对象
* @param response 响应对象
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 成功逻辑
response.setContentType("applicatoin/json;charset=utf-8");
response.getWriter().write("登录成功");
}
}
AuthenticationFailureHandler
接口
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
/**
* 登陆失败
* @param request 请求对象
* @param response 响应对象
* @param exception 失败和异常信息
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 失败逻辑
response.setContentType("applicatoin/json;charset=utf-8");
response.getWriter().write("登录成功");
}
}
@Data
abstract class ValidationCode implements Serializable {
/**
* 验证码
*/
private String code;
/**
* 有效时间
*/
private LocalDateTime expire;
/**
* 有参构造
* @param seconds 验证码存活时间
*/
ValidationCode(int seconds) {
// 执行子类的验证码生成逻辑
code = setValidationCode();
// 赋予存活时间
setExpire(seconds);
}
/**
* 验证码生成逻辑
*/
abstract protected String setValidationCode();
/**
* 判断验证时间是否有效
* @return true | false
*/
public boolean isExpire() {
return LocalDateTime.now().isAfter(getExpire());
}
/**
* 修改有效时间
* @param seconds 秒
*/
public void setExpire(int seconds) {
this.expire = LocalDateTime.now().plusSeconds(seconds);
}
}
setValidationCode()
@Data
public class ImageCode extends ValidationCode {
/**
* 验证码图片本身
*/
private BufferedImage image;
/**
* 构造方法直接继承父类
* @param seconds 存活时间
*/
public ImageCode(int seconds) {
super(seconds);
}
/**
* 编写生成图片验证码
* @return 验证码
*/
@Override
protected String setValidationCode() {
int width = 67;
int height = 23;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
// 随机生成文字
String sRand = "";
for (int i = 0; i < 4; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
// 修改图片信息
setImage(image);
// 给父类返回验证码
return sRand;
}
/**
* 生成随机背景条纹
*
* @param fc rgb色值
* @param bc rgb色值
* @return
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
OncePerRequestFilter
一次过滤
@Component
public class ImageCodeValidateFilter extends OncePerRequestFilter {
/**
* session 工具类
*/
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain fc) throws ServletException, IOException {
// 只应用于 /authentication/form 的 post
if(req.getMethod().equals("POST") && "/authentication/form".equals(req.getRequestURI())) {
// 取到验证码
ImageCode imageCode = (ImageCode)sessionStrategy.getAttribute(new ServletWebRequest(req),ValidataCodeController.VALIDATE_CODE_KEY);
// 验证码是否过期 || 验证码是否正确
if(imageCode.isExpire() || !Predicate.isEqual(imageCode.getCode()).test(req.getParameter("validateCode"))){
// 验证失败
resp.getWriter().write("error");
return;
}
}
// 验证成功,继续执行后续代码
fc.doFilter(req, resp);
}
}
@Controller
@RequestMapping("/validate")
public class ImageCodeController {
/**
* session 的工具类
*/
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* session 键名
*/
public static final String VALIDATE_CODE_KEY = "IMAGE_CODE_KEY";
/**
* 获取验证码接口
* @param req 请求对象
* @param resp 响应对象
* @throws IOException
*/
@GetMapping("/code")
public void validateCode(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 生成验证码
ImageCode imageCode = new ImageCode(60);
// 将ImageCode存入到session
sessionStrategy.setAttribute(new ServletWebRequest(req), VALIDATE_CODE_KEY, imageCode);
//将图片写入前端
ImageIO.write(imageCode.getImage(), "JPEG", resp.getOutputStream());
}
}
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.8</version>
</dependency>
<!-- 阿里云发送短信 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.0.3</version>
</dependency>
UsernamePasswordAuthenticationFilter
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public SmsAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/sms", "POST"));
}
// ~ Methods
// ========================================================================================================
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
// 反射
mobile = mobile.trim();
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
/**
* Provided so that subclasses may configure what is put into the authentication
* request's details property.
*
* @param request that an authentication request is being created for
* @param authRequest the authentication request object that should have its details
* set
*/
protected void setDetails(HttpServletRequest request,
SmsAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
/**
* Sets the parameter name which will be used to obtain the username from the login
* request.
*
* @param mobileParameter the parameter name. Defaults to "username".
*/
public void setUsernameParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Username parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
/**
* Defines whether only HTTP POST requests will be allowed by this filter. If set to
* true, and an authentication request is received which is not a POST request, an
* exception will be raised immediately and authentication will not be attempted. The
* <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
* authentication.
* <p>
* Defaults to <tt>true</tt> but may be overridden by subclasses.
*/
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return mobileParameter;
}
}
@Component
public class SmsCodeValidateFilter extends OncePerRequestFilter {
/**
* session 工具类
*/
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain fc) throws ServletException, IOException {
// 只应用于 /authentication/sms 的 post
if(req.getMethod().equals("POST") && "/authentication/sms".equals(req.getRequestURI())) {
// 取到验证码
SmsCode smsCode = (SmsCode)sessionStrategy.getAttribute(new ServletWebRequest(req), SmsCodeController.VALIDATE_CODE_KEY);
// 验证码是否过期 || 验证码是否正确
if(smsCode.isExpire() || !Predicate.isEqual(smsCode.getCode()).test(req.getParameter("smsCode"))){
// 验证失败
resp.getWriter().write("error");
return;
}
}
// 验证成功,继续执行后续代码
fc.doFilter(req, resp);
}
}
UsernamePasswordAuthenticationToken
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
/**
* 在验证之前封装电话信息,
* 在验证之后存储 用户信息
*/
private final Object principal;
// ~ Constructors
// ===================================================================================================
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*/
public SmsAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param authorities
*/
public SmsAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
AuthenticationProvider
接口
public class SmsAuthenticatoinProvider implements AuthenticationProvider {
private UserSecurityService userSecurityService;
/**
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken smsAuthenticationToken = (SmsAuthenticationToken)authentication;
// 获取到电话
String mobile = (String)smsAuthenticationToken.getPrincipal();
UserDetails userDetails = userSecurityService.loadUserByUsername(mobile);
SmsAuthenticationToken token = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
// 设置用户的其他的详细信息(登录一些)
token.setDetails(smsAuthenticationToken.getDetails());
return token;
}
/**
* 该方法就是来判断,该Provider要处理哪一个Filter丢过来的Token,
* 返回true, 上面的方法 authenticate(Authentication authentication)
*/
@Override
public boolean supports(Class<?> authentication) {
// 判断类型是否一致
return authentication.isAssignableFrom(SmsAuthenticationToken.class);
}
}
SecurityConfigurerAdapter
@Configuration
public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity> {
/**
* 登录逻辑
*/
@Resource
private UserSecurityService userSecurityService;
/**
* 登录成功
*/
@Resource
private MySuccessAuthenticationHandler successHandler;
/**
* 登录失败
*/
@Resource
private MyAuthenticationFailureHandler failureHandler;
/**
* 配置
* @param http 配置对象
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
// 创建自定义过滤实例
SmsAuthenticationFilter filter = new SmsAuthenticationFilter();
// 创建自定义provider实例
SmsAuthenticatoinProvider provider = new SmsAuthenticatoinProvider();
// 设置AuthenticatoinManager, 因为filter和Provider中间的桥梁就是 AuthenticationManager
filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 登录成功
filter.setAuthenticationSuccessHandler(successHandler);
// 登录失败
filter.setAuthenticationFailureHandler(failureHandler);
// 绑定登录查询逻辑
provider.setUserSecurityService(userSecurityService);
// 绑定provider
http.authenticationProvider(provider)
// 绑定过滤器
.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
}
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义的配置类
*/
@Resource
private SmsAuthenticationConfig smsAuthenticationConfig;
/**
* 手机验证码过滤器
*/
@Resource
private SmsCodeValidateFilter smsCodeValidateFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 三个修改
http.addFilterBefore()
// 添加手机验证码过滤器
.addFilterBefore(smsCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)
.xxx()
.and()
.xxx()
// 记得设置白名单
.antMatchers("/Sms/code")
.xxx()
.and()
.csrf().disable()
// 在这里接入 apply()
.apply(smsAuthenticationConfig);
}
}
public class SmsCode extends ValidationCode {
/**
* 直接使用父类构造
* @param seconds 存活时间
*/
public SmsCode(int seconds) {
super(seconds);
}
/**
* 定义验证码生成逻辑
* @return 验证码
*/
@Override
protected String setValidationCode() {
// 生成随机数子的工具类
RandomStringGenerator randomStringGenerator
= new RandomStringGenerator.Builder().withinRange(new char[]{'0','9'}).build();
return randomStringGenerator.generate(4);
}
}
SmsService
为第三个接口调用逻辑,不公开展示
@Service
public class SysUserService {
/**
* 封装好请求第三方接口的类
*/
@Resource
private SmsService smsService;
public boolean sendSms(String code) {
return smsService.send(code+"","mobile");
}
}
@Controller
@RequestMapping("/validate")
public class SmsCodeController {
/**
* service 层
*/
@Resource
private SysUserService sysUserService;
/**
* session 的工具类
*/
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* session 键名
*/
public static final String VALIDATE_CODE_KEY = "Sms_CODE_KEY";
/**
* 获取验证码接口
* @param req 请求对象
* @param resp 响应对象
* @throws IOException
*/
@GetMapping("/code")
public void validateCode(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 生成验证码
SmsCode SmsCode = new SmsCode(60);
// 将SmsCode存入到session
sessionStrategy.setAttribute(new ServletWebRequest(req), VALIDATE_CODE_KEY, SmsCode);
// 请求发送短信
sysUserService.sendSms();
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.7.0</version>
</dependency>
spring:
# 基本的 redis 配置
redis:
port: 6379
host: localhost
password:
# 连接池
lettuce:
pool:
min-idle: 2
max-active: 8
session:
# session 存储方式
store-type: redis
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.xxx()
.and()
.sessionManagement() //
.maximumSessions(1) // 表示一个用户只能有一个会话,不能重复登录
// 被强制下线时执行的逻辑
.expiredSessionStrategy(event -> {
HttpServletResponse response = event.getResponse();
response.setContentType("text/plain;charset=utf-8");
response.getWriter().write("您已经在其他设备登录,强制下线");
})
// 要用两个 and() 做结束
.and()
.and()
.xxx();
}
}
@Component
public class UserSecurityService implements UserDetailsService {
/**
* service 层
*/
@Resource
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
SysUser user = sysUserService.getUser(name);
// 实例化 User 时第三个参数是 List 类型的权限集合
User admin = new User(name, user.getPassword(),
// 必须 ROLE_ 前缀,实际情况是通过数据动态生成的
Arrays.asList(
new SimpleGrantedAuthority("ROLE_admin"),
new SimpleGrantedAuthority("ROLE_user")
)
);
return admin;
}
}
securedEnabled: 开启
@Secured("ROLE_abc")
方式
jsr250Enabled: 开启@RolesAllowed("admin")
方式
prePostEnabled: 开启@PreAuthorize
和@PostAuthorize
方式
@SpringBootApplication
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.xxx()
// 方式一:用户权限控制(推荐可用注解配置)
.antMatchers("/user/**", "/dept/**").hasRole("admin") // 不能有ROLE_前缀
// 方式二:操作权限控制(推荐用注解配置)
.antMatchers("/user/delete").hasRole("user:delete")
.and()
// 无权限访问时的处理
.exceptionHandling()
.accessDeniedHandler((req,resp,exception) -> {
// 处理逻辑
resp.sendRedirect("/noAuth.html");
})
.xxx();
}
}
@RolesAllowed
jsr250方式
@RestController
// 该类中所有方法都需要 user 权限
@RolesAllowed("user")
public class Demo {
@GetMapping("/TestSecurity")
// 该方法需要 admin 或 user 权限
@RolesAllowed({"admin","user"})
public String test() {
return "test";
}
}
@Secured
secured方式
public interface UserService {
@Secured("ROLE_user")
List<User> findAllUsers();
@Secured("ROLE_admin")
void deleteUser(int id);
}
@PreAuthorize
方法执行前
/**
* 对角色做权限控制
*/
@PreAuthorize("hasRole('admin')") // 需要角色 admin
@PreAuthorize("hasRole('admin') or hasRole('user')") // 需要角色 admin 或 user
@PreAuthorize("hasRole('admin') and hasRole('user')") // 同时需要角色 admin 和 user
@PreAuthorize("hasAnyRole('admin','user')") // 需要角色 admin 或 user
/**
* 对操作做权限控制
*/
@PreAuthorize("hasAuthority('user:delete')") // 需要有 user:delete 操作权限
@PreAuthorize("hasAuthority('user:delete') or hasAuthority('user:root')") // 需要有 user:delete 或 user:root 操作权限
@PreAuthorize("hasAuthority('user:delete') and hasAuthority('user:root')") // 同时需要 user:delete 和 user:root 操作权限
@PreAuthorize("hasAnyAuthority('user:delete','user:root')") // 需要有 user:delete 或 user:root 操作权限
@PostAuthorize
方法执行后@PreAuthorize
语法一样