SpringBoot2.0 + Oauth2 实现RESTful API身份验证

REST全称是Representational State Transfer; REST指的是一组架构约束条件和原则。有兴趣了解的朋友参考RESTful 架构详解;

OAuth2

SpringBoot2.0 + Oauth2 实现RESTful API身份验证_第1张图片
网页翻译后截图

配置授权服务oauth

@EnableAuthorizationServer: 用于激活OAuth 2.0授权服务器。
主要配置: 客户端信息, 管理令牌, 授权类型。
实现接口: AuthorizationServerConfigurer。
或者继承AuthorizationServerConfigurerAdapter。
其实查看AuthorizationServerConfigurerAdapter发现其本身就实现了AuthorizationServerConfigurer。

void configure(AuthorizationServerSecurityConfigurer security) throws Exception;

void configure(ClientDetailsServiceConfigurer clients) throws Exception;

void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception;  
   

具体配置例子:

/**
 * @Date 2019年5月21日
 * @Sgin AuthorizationServerConfig--认证服务核心配置
 * @Author Bertram.Wang
 */
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource(name = "dsOauth")
    DataSource dsOauth;
    @Autowired
    RedisConnectionFactory connectionFactory;
    @Autowired
    MyUserDetailsService userDetailsService;
    @Autowired
    AuthenticationManager authenticationManager;
    /**
     * 

设置令牌存储方式

* InMemoryTokenStore 在内存中存储令牌。 * RedisTokenStore 在Redis缓存中存储令牌。 * JwkTokenStore 支持使用JSON Web Key (JWK)验证JSON Web令牌(JwT)的子Web签名(JWS) * JwtTokenStore 不是真正的存储,不持久化数据,身份和访问令牌可以相互转换。 * JdbcTokenStore 在数据库存储,需要创建相应的表存储数据 */ @Bean public TokenStore tokenStore() { return new RedisTokenStore(connectionFactory); } /** *

设置密码校验器

* NoOpPasswordEncoder 直接文本比较 equals */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * *用来配置令牌端点(Token Endpoint)的安全约束。 */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients();//允许客户表单认证 security.passwordEncoder(passwordEncoder());//设置oauth_client_details中的密码编码器 security.checkTokenAccess("permitAll()");//对于CheckEndpoint控制器[框架自带的校验]的/oauth/check端点允许所有客户端发送器请求而不会被Spring-security拦截 } /** * *配置OAuth2的客户端相关信息。使用了数据库存储 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // jdbcTemplate 会查询指定数据源表: oauth_client_details; clients.jdbc(dsOauth); } /** * *配置授权服务器端点的属性和增强功能。 * *设置自定义验证规则, token存储设置使用... */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager) .tokenStore(tokenStore()) .userDetailsService(userDetailsService) // refreshToken是否可以重复使用。 默认:true; .reuseRefreshTokens(false); } }

注意:

  • 需要设置密码检验器;在用户创建时,需要注意密码密文的存储。
  • 身份验证管理升级后不能直接注入。需要添加一下Bean
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {   
    @Bean(name = "authenticationManager")
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    } 
} 
  • 刷新token时,refreshToken有效时间更新,实现一次登录一直使用。

    reuseRefreshTokens 设置为false。
    
  • MyUserDetailsService:自定义的身份验证逻辑实现接口 UserDetailsService
    示例:

/**
 * @Date 2019年5月21日
 * @Sgin MyUserDetailsService
 * @Author Bertram.Wang
 */
@Component
public class MyUserDetailsService implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(MyUserDetailsService.class);

    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String clientFlagValue;
        String name;
        try {
            // username 使用客户端ID凭借 == clientId:username
            String[] split = username.split(":");
            clientFlagValue = split[0];
            name = split[1];
        } catch (Exception e) {
            log.error("用户名请拼接资源服务标识");
            return null;
        }
        
        MyUserDetails userDetails = new MyUserDetails();
        userDetails.setUsername(username);
        // 根据标识区分具体的客户端
        OauthClientFlagEnum clientFlagEnum = OauthClientFlagEnum.build(clientFlagValue);
        switch (clientFlagEnum) {
        case VUE_WEB:
            User user = userRepository.oneByName(name);
            if (user == null) {
                throw new UsernameNotFoundException(String.format("User %s does not exist!", username));
            }
            userDetails.setUserId(user.getId().toString());
            userDetails.setPassword(user.getPassword());
            break;
        case MEMBER_API:
            Member member = memberRepository.oneByPhone(name);
            if (member == null) {
                throw new UsernameNotFoundException(String.format("User %s does not exist!", username));
            }
            userDetails.setUserId(member.getId().toString());
            userDetails.setPassword(member.getPassword());
            break;
        default:
            log.error("用户名请拼接资源服务标识==clientFlagValue:{}", clientFlagValue);
            return null;
        }
        return userDetails;
    }
    /**
     *  *用于存储用户信息
     */
    @Data
    private final static class MyUserDetails implements UserDetails {
        private static final long serialVersionUID = 1L;
        private String username;
        private String password;
        private String userId;
        private MyUserDetails() {
            super();
        }
        @Override
        public Collection getAuthorities() {
            Role role = new Role();
            return role.getRoles();
        }
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
}
  • 客户端表
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(48) NOT NULL,
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `authorized_grant_types` varchar(256) DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

客户端服务

首先配置所有连接都可以访问。

@Configuration
// 注解开启Spring Security的功能
@EnableWebSecurity 
// 开启自动配置
@EnableAutoConfiguration
// 启用了一个Oauth2 客户端配置
@EnableOAuth2Client
public class ActuatorWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable() // 禁用csrf支持
            // 允许使用 HttpServletRequest 限制访问
            .authorizeRequests() 
            // 任何人都可以使用任何URL资源
            .antMatchers("/**").permitAll();
    }
}

添加自定义拦截器验证token

/**
 * @describe 拦截器 -- oauth验权
 * @author Bertram.Wang
 * @date 2018年10月16日 下午8:58:17
 */
public class Oauth2Interceptor implements HandlerInterceptor {

    public static final Logger LOGGER = LoggerFactory.getLogger(Oauth2Interceptor.class);
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        String accessToken;
        String authorization = request.getHeader("Authorization");
        if (StringUtils.isEmpty(authorization)) {
            ResponseUtils.writeResponseData(response, fail(MyReponseStatus.AUTH_LACK));
            return false;
        }
        try {
            accessToken = new String(Base64.getDecoder().decode(authorization)).split(":")[1];
        } catch (Exception e) {
            LOGGER.error(e.getMessage());
            ResponseUtils.writeResponseData(response, fail(MyReponseStatus.AUTH_LACK));
            return false;
        }
        
        OAuth2AccessToken oauth2AccessToken = Oauth2Utils.checkTokenInOauthClient(accessToken);
        
        if (oauth2AccessToken == null) {// 非法的Token值
            ResponseUtils.writeResponseData(response, fail(MyReponseStatus.AUTH_LACK));
            return false;
        } else if (oauth2AccessToken.isExpired()) {// token失效
            ResponseUtils.writeResponseData(response, fail(MyReponseStatus.AUTH_LACK));
            return false;
        }
        
        Map additionalInformation = oauth2AccessToken.getAdditionalInformation();
        int memberId = Integer.parseInt(additionalInformation.get("userId").toString());
        request.setAttribute("memberId", memberId);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
    }

    protected static String extractTokenKey(String value) {
        if (value == null) {
            return null;
        }
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("MD5 algorithm not available.  Fatal (should be in the JDK).");
        }

        try {
            byte[] bytes = digest.digest(value.getBytes("UTF-8"));
            return String.format("%032x", new BigInteger(1, bytes));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException("UTF-8 encoding not available.  Fatal (should be in the JDK).");
        }
    }

}

注册拦截器

/**
 * @describe WEBMVC配置
 * @author Bertram.Wang
 * @date 2018年10月16日 下午8:56:49
 */
@Configuration
public class InterceptorRegisterConfiguration implements WebMvcConfigurer{

    @Bean
    public Oauth2Interceptor oauth2Interceptor() {
        return new Oauth2Interceptor();
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(oauth2Interceptor())
        // 排除指定连接不做拦截
        .excludePathPatterns("/swagger-resources/**", 
                             "/webjars/**", 
                             "/swagger-ui.html/**", 
                             "/docs", 
                             "/account/**", 
                             "/auth/**" );
    }
}

完成OAuth身份验证。

你可能感兴趣的:(SpringBoot2.0 + Oauth2 实现RESTful API身份验证)