从头开始spring security oauth 2.0 (一)

之前公司大佬让我用oauth 2的标准开发一套资源授权系统。 阅读了很多资料,部分展示在参考资料那趴。 因为spring security oauth 2.0设计的权限管理表格比较复杂,所以楼主实现了自定义用户表、客户端表。此版本缺少权限分配

资源

  • code repository
  • api doc

技术栈

  • spring boot 2.0
  • spring security oauth 2.0
  • spring jpa
  • keytool
  • jwt
  • mysql

开发


一、架构

从头开始spring security oauth 2.0 (一)_第1张图片

这是我习惯的架构方式,根据爱好自己分配。

二、依赖工具

  • lombok
    • 参考文档:https://blog.csdn.net/zhglance/article/details/54931430
  • 阿里代码规范插件(非必须)
    • 参考文档:https://github.com/alibaba/p3c

三、基础项目配置

1. 跨域

1)在配置文件中添加类,CorsFilter

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = ((HttpServletResponse) servletResponse);
        HttpServletRequest request = (HttpServletRequest) servletRequest;

        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods",
                "ACL, CANCELUPLOAD, CHECKIN, CHECKOUT, COPY, DELETE, GET, HEAD, LOCK, MKCALENDAR, MKCOL, MOVE, OPTIONS, POST, PROPFIND, PROPPATCH, PUT, REPORT, SEARCH, UNCHECKOUT, UNLOCK, UPDATE, VERSION-CONTROL");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers",
                "Origin, X-Requested-With, Content-Type, Accept, Key, Authorization");
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // not needed
    }

    @Override
    public void destroy() {
        // not needed
    }
}

由于三次握手,options的请求方式要直接通过。即:在正式跨域的请求前,浏览器会根据需要,发起一个“PreFlight”(也就是Option请求),用来让服务端返回允许的方法(如get、post),被跨域访问的Origin(来源,或者域),还有是否需要Credentials(认证信息

2. 异常处理

1)自定义异常的类型

@Getter
public enum ResultEnum {
    #示例
    SUCCESS(100, "返回成功"),

    MISS_REQUEST_PARAM_ERROR(101, "缺少请求参数"),

    SAVE_USERNAME_EXIST_ERROR(102, "用户已存在!"),

    SAVE_CLIENT_EXIST_ERROR(103, "客户端已存在!"),

    PUBLIC_KEY_NOT_EXIST(104, "公钥不存在!")

    ;

    private Integer code;

    private String message;

    ResultEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

2)同一定义返回的vo对象

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result {

    /** 错误码 */
    private Integer code;

    /** 提示信息 */
    private String msg;

    /** 具体内容 */
    private T data;
}

3)自定义VO工具

public class ResultUtil {

    public static Result success(Object object) {
        Result result = new Result<>();
        result.setData(object);
        result.setCode(ResultEnum.SUCCESS.getCode());
        result.setMsg(ResultEnum.SUCCESS.getMessage());
        return result;
    }

    public static Result success() {
        return success(null);
    }

    public static Result error(ResultEnum resultEnum) {
        Result result = new Result();
        result.setCode(resultEnum.getCode());
        result.setMsg(resultEnum.getMessage());
        return result;
    }

    public static Result error(Integer code, String message) {
        Result result = new Result();
        result.setCode(code);
        result.setMsg(message);
        return result;
    }
}
 
 

3)自定义异常

@Getter
public class MyException extends RuntimeException {

    private Integer code;

    public MyException(ResultEnum resultEnum) {
        super(resultEnum.getMessage());
        this.code = resultEnum.getCode();
    }

    public MyException(Integer code, String msg) {
        super(msg);
        this.code = code;
    }

    public MyException(ResultEnum resultEnum, String msg) {
        super(resultEnum.getMessage().concat(msg));
        this.code = resultEnum.getCode();
    }
}

4)自定义异常处理

@Slf4j
@ControllerAdvice
public class ExceptionHandle {

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result handle(Exception e) {

        if (e instanceof MyException) {
            MyException myException = (MyException) e;
            log.error(myException.toString());
            return ResultUtil.error(myException.getCode(), e.getMessage());
        } else if (e instanceof MissingServletRequestParameterException) {
            return ResultUtil.error(ResultEnum.MISS_REQUEST_PARAM_ERROR);
        } else {
            log.error("【系统异常】{}", e.toString());
            e.printStackTrace();
            return ResultUtil.error(-1, e.getMessage());
        }
    }
}

至此,项目的基础工具搭建完毕。

四、授权服务器配置 (Authorization Server)

1)原理
从头开始spring security oauth 2.0 (一)_第2张图片

2)数据库表设计
出于方便维护的出发点,这里使用spring data jpa的方式来生成并管理表

#用户表
@Data
@Entity
@Table(name = "oauth_user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /** 用户名 */
    @Column(nullable = false, unique = true)
    private String username;

    /** 密码 */
    private String password;

    /** 是否可用 */
    private Boolean enabled;

    /** 是否被锁 */
    private Boolean noLocked;
}
@Data
@Entity
@Table(name = "oauth_client")
public class Client {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "client_id", unique = true)
    private String clientId;

    private String resourceIds;

    private String clientSecret;

    private Boolean secretRequire;

    private String scope;

    private Boolean scopeRequire;

    private String authorizedGrantTypes;

    private String authorities;

    private Integer accessTokenValidity;

    private Integer refreshTokenValidity;
}

当然,如果你喜欢用sql文件来管理,那么将是如下:

-- table for client details
DROP TABLE IF EXISTS  `oauth_client`;
CREATE TABLE `oauth_client` (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  client_id VARCHAR(256) UNIQUE NOT NULL COMMENT 'store the id of newly registered clients',
  resource_ids VARCHAR(256) NOT NULL COMMENT 'store the password of clients',
  client_secret VARCHAR(256) DEFAULT '' COMMENT 'which indicates if client is still valid',
  secret_require BIT NOT NULL DEFAULT 1,
  scope VARCHAR(256) DEFAULT '' COMMENT 'allowed actions, for example writing statuses on Facebook etc.',
  scope_require BIT NOT NULL DEFAULT 1,
  authorized_grant_types VARCHAR(256) NOT NULL COMMENT 'which provides information how users can login to the particular client (in our example case it’s a form login with password)',
  authorities VARCHAR(256) NOT NULL COMMENT '授权',
  access_token_validity INTEGER COMMENT '',
  refresh_token_validity INTEGER COMMENT ''
);

-- user
DROP TABLE IF EXISTS `oauth_user`;
CREATE TABLE `oauth_user` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (id),
  `username` VARCHAR(255) UNIQUE NOT NULL,
  `password` VARCHAR(255),
  `enabled` BIT NOT NULL DEFAULT 1 COMMENT '是否有效',
  `no_locked` BIT NOT NULL DEFAULT 1 COMMENT '是否锁定账户'
);

3)repository(mapper) 配置
楼主用的jpa,所以直接继承JpaRepository。或者也可以用mybatis的mapper映射数据库的数据。

public interface ClientRepository extends JpaRepository {

    /**
     * 按clientId查找client
     * @param clientId
     * @return
     */
    Client findByClientId(String clientId);
}
public interface UserRepository extends JpaRepository {

    /**
     * 按名称查询用户
     * @param username
     * @return
     */
    User findByUsername(String username);
}

4)定义principal
定义clientPrincipal和userPrincipal用于spring security的数据读取、使用。

public class MyUserPrincipal implements UserDetails {

    //TODO:等待完善权限分配...

    private User user;

    public MyUserPrincipal(User user) {
        this.user = user;
    }

    @Override
    public Collection getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return user.getNoLocked();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return user.getEnabled();
    }
}
public class MyClientPrincipal implements ClientDetails {

    //TODO:等待完善...

    private Client client;

    public MyClientPrincipal(Client client) {
        this.client = client;
    }

    @Override
    public String getClientId() {
        return client.getClientId();
    }

    @Override
    public Set getResourceIds() {
        return new HashSet<>(Arrays.asList(client.getResourceIds().split(Constant.SPLIT_COMMA)));
    }

    @Override
    public boolean isSecretRequired() {
        return client.getSecretRequire();
    }

    @Override
    public String getClientSecret() {
        return client.getClientSecret();
    }

    @Override
    public boolean isScoped() {
        return client.getScopeRequire();
    }

    @Override
    public Set getScope() {
        return new HashSet<>(Arrays.asList(client.getScope().split(Constant.SPLIT_COMMA)));
    }

    @Override
    public Set getAuthorizedGrantTypes() {
        return new HashSet<>(Arrays.asList(client.getAuthorizedGrantTypes().split(Constant.SPLIT_COMMA)));
    }

    @Override
    public Set getRegisteredRedirectUri() {
        return null;
    }

    @Override
    public Collection getAuthorities() {
        Collection collection = new ArrayList<>();
        Arrays.asList(client.getAuthorities().split(Constant.SPLIT_COMMA)).forEach(
                auth -> collection.add((GrantedAuthority) () -> auth)
        );
        return collection;
    }

    @Override
    public Integer getAccessTokenValiditySeconds() {
        return client.getAccessTokenValidity();
    }

    @Override
    public Integer getRefreshTokenValiditySeconds() {
        return client.getRefreshTokenValidity();
    }

    @Override
    public boolean isAutoApprove(String scope) {
        return false;
    }

    @Override
    public Map getAdditionalInformation() {
        return null;
    }
}

5)service层
自定义service继承spring security的UserDetailsService和ClientDetailsService。目的是使用自定义的mysql表管理用户和客户端。
它们的实现如下:

@Service
public class MyUserDetailsServiceImpl implements MyUserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        }
        return new MyUserPrincipal(user);
    }

    @Override
    public User save(User user) {
        //1、如果已存在,则抛出异常
        if (userRepository.findByUsername(user.getUsername()) != null) {
            throw new MyException(ResultEnum.SAVE_USERNAME_EXIST_ERROR);
        }
        //2、否则,添加
        return userRepository.save(user);
    }
}
@Service
public class MyClientDetailsServiceImpl implements MyClientDetailsService {

    @Autowired
    private ClientRepository clientRepository;

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        Client client = clientRepository.findByClientId(clientId);
        if (client == null) {
            throw new ClientRegistrationException(clientId);
        }
        return new MyClientPrincipal(client);
    }

    @Override
    public Client save(Client client) {
        //1、如果已存在,抛出异常
        if (clientRepository.findByClientId(client.getClientId()) != null) {
            throw new MyException(ResultEnum.SAVE_CLIENT_EXIST_ERROR);
        }
        //2、否则,添加
        return clientRepository.save(client);
    }
}

6)controller层
passwordEncoder先不用管,后续会添加到bean中。

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private MyUserDetailsService userService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @RequestMapping("/create")
    public User create(@RequestParam String username,
                       @RequestParam String password) {
        User user = new User();
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(password));
        user.setEnabled(true);
        user.setNoLocked(true);
        return userService.save(user);
    }
}
@RestController
@RequestMapping("/client")
public class ClientController {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private MyClientDetailsService myClientDetailsService;

    @RequestMapping("/create")
    public Client create(@RequestParam String clientId,
                         @RequestParam String resourceIds,
                         @RequestParam(required = false, defaultValue = "") String clientSecret,
                         @RequestParam String scope,
                         @RequestParam String authorities,
                         @RequestParam String authorizedGrantTypes,
                         @RequestParam(required = false, defaultValue = Constant.DEFAULT_ACCESS_TOKEN_VALIDITY) Integer accessTokenValidity,
                         @RequestParam(required = false, defaultValue = Constant.DEFAULT_REFRESH_TOKEN_VALIDITY) Integer refreshTokenValidity
    ) {
        Client client = new Client();
        client.setClientId(clientId);
        client.setResourceIds(resourceIds);
        client.setClientSecret(passwordEncoder.encode(clientSecret));
        client.setSecretRequire(!"".equals(clientSecret));
        client.setScope(scope);
        client.setScopeRequire(!"".equals(scope));
        client.setAuthorities(authorities);
        client.setAuthorizedGrantTypes(authorizedGrantTypes);
        client.setAccessTokenValidity(accessTokenValidity);
        client.setRefreshTokenValidity(refreshTokenValidity);
        return myClientDetailsService.save(client);
    }
}
#测试使用
@RestController
@RequestMapping("/order")
public class TestController {

    @GetMapping("/test")
    public Map test(OAuth2Authentication auth) {

        Map result = new HashMap<>(3);

        OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();

        result.put("userId", auth.getName());
        result.put("userIP", details.getRemoteAddress());
        result.put("userRole", auth.getAuthorities());
        return result;
    }
}

7)配置文件

基本的spring security的配置,值得注意的是spring security 5.0后authenticationManager需要手动注入到bean里面:

@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated()
                .and()

                .httpBasic()
                .and()

                .csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }

    /** 将AuthenticationManager暴露到bean中 */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder);
        return authProvider;
    }
}

授权服务器自身的资源服务器配置(为了管理用户和客户端的增删改查):

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId(Constant.RESOURCE_ID_NORMAL_APP)
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                // Since we want the protected resources to be accessible in the UI as well we need
                // session creation to be allowed (it's disabled by default in 2.0.6)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .requestMatchers().anyRequest()
                .and()
                //允许匿名登陆
                .anonymous()
                .and()
                .authorizeRequests()
//                    .antMatchers("/product/**").access("#oauth2.hasScope('select') and hasRole('ROLE_USER')")
                //配置order访问控制,必须认证过后才可以访问
                .antMatchers("/user/**").permitAll()
                .antMatchers("/order/**").authenticated();
    }
}

授权服务器配置,关于其中密钥文件,生成方法详见从头开始spring security oauth 2.0 (二):

@Slf4j
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private MyClientDetailsService clientDetailsService;

    /**
     * 客户端访问配置
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints
                .authenticationManager(authenticationManager)
                .accessTokenConverter(accessTokenConverter())
                .tokenStore(tokenStore())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET)
                .userDetailsService(userDetailsService);
    }

    /**
     * 配置客户端详情
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
    }

    /**
     * 配置AuthorizationServer安全认证的相关信息
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
    }

    // -------------次要配置⚠️每个资源服务都需要配置以下内容!!!-------------------

    /**
     * 加密方式
     * @return
     */
    @Bean
    public static PasswordEncoder passwordEncoder() {
        String idForEncode = "bcrypt";
        Map encoders = new HashMap<>(1);
        encoders.put(idForEncode, new BCryptPasswordEncoder());
        return new DelegatingPasswordEncoder(idForEncode, encoders);
    }

    /**
     * 配置AccessToken的存储方式、
     * 可选择的存储方式是:
     * 1、InMemoryTokenStore
     * 2、JdbcTokenStore
     * 3、JwtTokenStore
     * 4、RedisTokenStore
     * 5、JwkTokenStore
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    /**
     * token converter
     * @return
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {

        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();

        /*
         * 授权服务配置
         */
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
                new ClassPathResource("jwt-key.jks"),
                "wcm520".toCharArray()
        );
        accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("wcm-key"));

        /*
         * 对应的资源服务配置
            Resource resource = new ClassPathResource("public.txt");
            String publicKey = null;
            try {
                publicKey = IOUtils.toString(resource.getInputStream());
            } catch (final IOException e) {
                throw new MyException(ResultEnum.PUBLIC_KEY_NOT_EXIST);
            }
            accessTokenConverter.setVerifier(publicKey);
         */
        return accessTokenConverter;
    }
}

参考资料


  • 阮一峰 - 理解OAuth 2.0
  • csdn - 下一秒升华 - 从零开始的Spring Security Oauth2

你可能感兴趣的:(从头开始spring security oauth 2.0 (一))