最近在考虑增强系统的安全性,主要使用 spring boot security & spring boot oauth2 来保证系统的安全。

主要依赖(Dependence)

<!-- Spring Security -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency<!-- Spring Security OAuth2 -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

全局依赖配置(GlobalAuthenticationConfigurerAdapter)

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.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableGlobalAuthentication
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
public class GlobalSecurityConfig extends GlobalAuthenticationConfigurerAdapter {

  @Autowired
  private UserDetailsService userDetailsService;

  @Override
  public void init(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
}

用户控制器(UserController)

    package cn.edu.cqjtu.oj.web.v1;

    import java.util.List;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.security.core.Authentication;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import com.fasterxml.jackson.annotation.JsonView;

    import cn.edu.cqjtu.oj.domain.TbUser;
    import cn.edu.cqjtu.oj.domain.TbUser.UserSimpleView;
    import cn.edu.cqjtu.oj.service.UserService;
    import cn.edu.cqjtu.oj.support.OAuth2User;
    import cn.edu.cqjtu.oj.support.UserQueryCondition;
    import io.swagger.annotations.ApiOperation;

    /**
    * 用户控制器
    *
    * @author johnniang
    *
    */
    @RestController
    @RequestMapping("/api/v1/users")
    public class UserController {

      private Logger logger = LoggerFactory.getLogger(getClass());

      @Autowired
      private UserService userService;

      // @Autowired
      // private MyValidator validator;

      @GetMapping
      @ApiOperation("分页查询用户")
      @JsonView(UserSimpleView.class)
      @PreAuthorize("hasAuthority('ROLE_ADMIN')")
      public List<TbUser> list(UserQueryCondition condition) {
        // 查询符合条件的用户
        List<TbUser> users = userService.query(condition);
        return users;
      }

      @GetMapping("/me")
      @ApiOperation("查询用户详情")
      @JsonView(UserSimpleView.class)
      public OAuth2User getDetails(Authentication authentication) {
        if (logger.isDebugEnabled()) {
          logger.debug("Principal class: {}", authentication.getClass());
        }
        return (OAuth2User) authentication.getPrincipal();
      }

    }

遇到的问题(Problem)

在 UserController 中一旦加入了@PreAuthorize("hasAuthority('ROLE_ADMIN')")注解,将会导致 Spring 扫描不到 UserController,最终没办法访问此 URL,所以将会出现 404 错误。

解决方案(Solution)

搞了两天了,已经无计可施了。各种 Google / Baidu,没有任何一个解决方案适合这个问题。

    package cn.edu.cqjtu.oj.web;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import cn.edu.cqjtu.oj.web.v1.UserController;

    @RestController
    @RequestMapping("/test")
    public class TestController {

      Logger logger = LoggerFactory.getLogger(getClass());

      @Autowired
      private UserController userController;

      @GetMapping
      public void testUserController() {
        if (logger.isDebugEnabled()) {
          logger.debug("User Controller: {}", userController);
        }
      }
    }

最后自己写了一个 TestController,就直接用使用 Logger 打印了一下 userController 这个对象。问题终于浮出水面:

    ***************************
    APPLICATION FAILED TO START
    ***************************

    Description:

    The bean 'userController' could not be injected as a 'cn.edu.cqjtu.oj.web.v1.UserController' because it is a JDK dynamic proxy that implements:


    Action:

    Consider injecting the bean as one of its interfaces or forcing the use of CGLib-based proxies by setting proxyTargetClass=true on @EnableAsync and/or @EnableCaching.

这不就是使用了 JDK 自身的代理么,导致没办法使用 AOP 进行监控方法的执行权限。

最后一根救命稻草终于结束了这么久这么长久的搜寻工作。

spring:
  aop:
    proxy-target-class: true