前言

关于OAuth2的名词概念说明和可以移步理解OAuth 2.0 - 阮一峰,这是一篇对于OAuth2很好的科普文章。本文主要实现SpeingBoot2.x+OAuth2+JWT对微服务进行认证授权、权限校验。本文仅涉及password模式,因为公司项目不涉及第三方登录,其他模式逻辑相似,可自行完善。

一、核心依赖

<!-- spring security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring security oauth2 -->
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.7.RELEASE</version>
</dependency>
<!-- spring security jwt -->
<dependency>
    groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.9.RELEASE</version>
</dependency>

二、认证授权服务器

1.认证服务核心配置

/**
 * @Auther: zlx
 * @Date: 2020/4/18 16:04
 * @Description: 授权服务配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private JwtTokenStore jwtTokenStore;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Qualifier("jwtAccessTokenConverter")
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired
    private PasswordEncoder passwordEncoder;

    //1.客户端详情服务
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient(CustomConstants.OAUTH2_CLIENT_ID)// 客户端id
                .secret(passwordEncoder.encode(CustomConstants.OAUTH2_CLIENT_SECRET))//客户端密钥
                .resourceIds(CustomConstants.OAUTH2_CLIENT_RESOURCE_ID)//资源列表
                .authorizedGrantTypes(CustomConstants.OAUTH2_GRANT_TYPE)// 该client允许的授权类型password
                .scopes(CustomConstants.OAUTH2_SCOPE)// 允许的授权范围
                .autoApprove(true);//true表示直接发放令牌
    }

    //2.配置令牌访问端点
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .authenticationManager(authenticationManager)//密码模式需要配置
                .tokenServices(tokenService())//令牌管理服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET);//请求令牌运行post方式
        // 自定义异常转换类
        endpoints.exceptionTranslator(new CustomWebResponseExceptionTranslator());
    }

    //3.配置令牌访问端点的安全约束
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        //注入自定义过滤器
        String path = CustomConstants.OAUTH2_DEFAULT_TOKEN_PATH;
        CustomClientCredentialsTokenEndpointFilter endpointFilter = new CustomClientCredentialsTokenEndpointFilter(security, path);
        endpointFilter.afterPropertiesSet();
        endpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint());
        security.addTokenEndpointAuthenticationFilter(endpointFilter);
        security.authenticationEntryPoint(authenticationEntryPoint());

        security
                .tokenKeyAccess("permitAll()")                    //oauth/token_key是公开
                .checkTokenAccess("isAuthenticated()");          //oauth/check_token需要登录


    }

    //令牌管理服务
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service = new DefaultTokenServices();
        service.setSupportRefreshToken(false);//支持刷新令牌
        service.setReuseRefreshToken(false);//是否复用refresh_token,默认为true(如果为false,则每次请求刷新都会删除旧的refresh_token,创建新的refresh_token)
        service.setTokenStore(jwtTokenStore);//令牌存储策略
        service.setAccessTokenValiditySeconds(CustomConstants.OAUTH2_TOKEN_VALIDITY_SECONDS); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(CustomConstants.OAUTH2_REFRESH_TOKEN_VALIDITY_SECONDS); // 刷新令牌默认有效期3天
        //令牌增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(jwtAccessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);
        return service;
    }

    //在认证服务器注入异常处理逻辑,自定义异常返回结果
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return (request, response, e) -> {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setStatus(HttpStatus.OK.value());
            response.setHeader("Content-Type", "application/json;charset=UTF-8");
            response.getWriter().write(JSONUtil.toJsonPrettyStr(Result.failure(ResultStatus.CLIENT_AUTHENTICATION_FAILED.getCode(), ResultStatus.CLIENT_AUTHENTICATION_FAILED.getMessage())));
        };
    }
}

2.安全服务配置

/**
 * @Auther: zlx
 * @Date: 2020/4/18 16:04
 * @Description: 安全服务配置
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailServiceImpl userDetailService;

    //认证管理器 密码模式必须配
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        return daoAuthenticationProvider;
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().and().csrf().disable()
                .authorizeRequests().antMatchers("/oauth/**").permitAll()
                .and()
                .authorizeRequests().anyRequest().authenticated();
    }
}

3.配置token生成方案,使用JWT生成令牌

/**
 * @Auther: zlx
 * @Date: 2020/4/18 16:28
 * @Description: token配置
 */
@Configuration
public class TokenConfig {

    //JWT令牌存储方案
    @Bean
    public JwtTokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(CustomConstants.OAUTH2_JWT_SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }
}

4.重写客户端认证过滤器,自定义异常处理,不使用默认的 OAuth2AuthenticationEntryPoint处理异常

/**
 * @Auther: zlx
 * @Date: 2021/3/8 13:09
 * @Description: 重写客户端认证过滤器,不使用默认的 OAuth2AuthenticationEntryPoint处理异常
 */
public class CustomClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter {

    private final AuthorizationServerSecurityConfigurer configurer;

    private AuthenticationEntryPoint authenticationEntryPoint;

    public CustomClientCredentialsTokenEndpointFilter(AuthorizationServerSecurityConfigurer configurer, String path) {
        this.configurer = configurer;
        setFilterProcessesUrl(path);
    }

    @Override
    public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
        super.setAuthenticationEntryPoint(null);
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    @Override
    protected AuthenticationManager getAuthenticationManager() {
        return configurer.and().getSharedObject(AuthenticationManager.class);
    }

    @Override
    public void afterPropertiesSet() {
        setAuthenticationFailureHandler((request, response, e) -> authenticationEntryPoint.commence(request, response, e));
        setAuthenticationSuccessHandler((request, response, authentication) -> {
        });
    }
}

5.自定义异常响应格式

/**
 * @Auther: zlx
 * @Date: 2021/3/5 20:52
 * @Description: 自定义异常类
 */
@Slf4j
public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator {

    @Override
    public ResponseEntity<Result<Void>> translate(Exception e) {
        Result<Void> response = resolveException(e);
        return new ResponseEntity<>(response, HttpStatus.valueOf(response.getCode()));
    }

    /**
     * 构建返回异常
     */
    private Result<Void> resolveException(Exception e) {
        // 初始值 500
        ResultStatus resultStatus = ResultStatus.ERROR;
        //不支持的认证方式
        if (e instanceof UnsupportedGrantTypeException) {
            resultStatus = ResultStatus.UNSUPPORTED_GRANT_TYPE;
            //用户名或密码异常
        } else if (e instanceof InvalidGrantException || e instanceof InternalAuthenticationServiceException) {
            resultStatus = ResultStatus.USERNAME_OR_PASSWORD_ERROR;
        } else if (e instanceof DisabledException) {
            resultStatus = ResultStatus.DISABLED_EXCEPTION;
        } else if (e instanceof LockedException) {
            resultStatus = ResultStatus.LOCKED_EXCEPTION;
        } else if (e instanceof AccountExpiredException) {
            resultStatus = ResultStatus.ACCOUNT_EXPIRED_EXCEPTION;
        } else if (e instanceof CredentialsExpiredException) {
            resultStatus = ResultStatus.CREDENTIALS_EXPIRED_EXCEPTION;
        }
        return Result.failure(resultStatus.getCode(), resultStatus.getMessage());
    }
}

6.重写用户认证信息,实现UserDetailsService接口

/**
 * @Auther: zlx
 * @Date: 2020/4/18 16:04
 * @Description: 查询用户信息
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserMapper userMapper;

    @Autowired
    private SysMenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = userMapper.selectOne(new QueryWrapper<SysUser>().eq("username", username));
        if (sysUser == null) {
            throw new CustomException("用户不存在");
        }
        //获取所有角色
        List<String> permissionList = menuMapper.selectMenuUrlByUserId(sysUser.getId());
        if (CollectionUtils.isEmpty(permissionList)) {
            throw new CustomException("该用户未分配权限");
        }
        Oauth2User oauth2User = new Oauth2User();
        BeanUtils.copyProperties(sysUser, oauth2User);
        oauth2User.setEnabled(sysUser.getStatus() == 0);
        oauth2User.setAuthorities(AuthorityUtils.createAuthorityList(permissionList.toArray(new String[0])));

        if (!oauth2User.isEnabled()) {
            throw new DisabledException("该账户已被禁用!");
        } else if (!oauth2User.isAccountNonLocked()) {
            throw new LockedException("该账号已被锁定!");
        } else if (!oauth2User.isAccountNonExpired()) {
            throw new AccountExpiredException("该账号已过期!");
        } else if (!oauth2User.isCredentialsNonExpired()) {
            throw new CredentialsExpiredException("该账户的登录凭证已过期,请重新登录!");
        }
        return oauth2User;
    }

}

三、资源服务器

1.资源服务器核心配置

/**
 * @Auther: zlx
 * @Date: 2020/4/18 18:31
 * @Description: 资源服务器
 */
@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private JwtTokenStore tokenStore;

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;

    @Autowired
    private AuthExceptionEntryPoint authExceptionEntryPoint;

    @Autowired
    private OAuth2WebSecurityExpressionHandler expressionHandler;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(CustomConstants.OAUTH2_CLIENT_RESOURCE_ID)//认证服务器配置的资源id
                .tokenStore(tokenStore)//jwt验证token
                .accessDeniedHandler(customAccessDeniedHandler)//自定义权限校验异常
                .authenticationEntryPoint(authExceptionEntryPoint)//自定义token校验异常
                .stateless(true).expressionHandler(expressionHandler);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests().antMatchers("/oauth/**", CustomConstants.OAUTH2_CUSTOM_TOKEN_PATH).permitAll()//放行接口
                .and()
                .authorizeRequests().antMatchers("/user/getUserInfo").authenticated()//只要认证通过就可以访问的接口
                .and()
                .authorizeRequests().antMatchers("/**").access("@security.hasPermission(authentication, request)");//其他所有接口走自定义权限判断
    }

    @Bean
    public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler(ApplicationContext applicationContext) {
        OAuth2WebSecurityExpressionHandler expressionHandler = new OAuth2WebSecurityExpressionHandler();
        expressionHandler.setApplicationContext(applicationContext);
        return expressionHandler;
    }
}

2.自定义权限判断

@Component("security")
public class SecurityService {

    /**
     * @return 返回true放行 false会被自定义的权限校验异常处理
     * {@link com.equipment.life.cycle.oauth2.resource.expection.CustomAccessDeniedHandler}
     */
    public boolean hasPermission(Authentication authentication, HttpServletRequest request) {
        String requestURI = request.getRequestURI();//当前请求的URL
        //判断当前请求的URL是否在用户所拥有的权限中
        return authentication
                .getAuthorities()
                .stream()
                .anyMatch((u) -> u.getAuthority().equals(requestURI));
    }
}

3.自定义token校验失败异常返回格式

@Component
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint {

    @SneakyThrows
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
        response.setStatus(HttpStatus.OK.value());
        response.setHeader("Content-Type", "application/json;charset=UTF-8");
        System.out.println(authException.getMessage());
        try {
            response.getWriter().write(JSONUtil.toJsonPrettyStr(Result.failure(ResultStatus.TOKEN_VERIFICATION_FAILED)));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.自定义权限校验异常返回格式

@Component("customAccessDeniedHandler")
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
        response.setStatus(HttpStatus.OK.value());
        response.setHeader("Content-Type", "application/json;charset=UTF-8");
        try {
            response.getWriter().write(JSONUtil.toJsonPrettyStr(Result.failure(ResultStatus.FULL_AUTHENTICATION)));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

四、测试效果图

(1)获取token成功

成功获取token

(2)获取token失败

(3)获取用户信息

(4)请求资源接口成功

(5)请求资源接口失败

 
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐