SpringCloud OAuth2 JWT认证 多种登录方式

4,466

前言

在微服务中,一般都会有一个专门的认证服务,在这个认证服务上完成认证后就可以直接访问其他服务,这种效果仅使用SpringSecurity是比较难实现的,本文将使用SpringCloud OAuth2+JWT的方式来实现。

依赖

先导入相关依赖

<!--核心两个依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
<!--其他依赖-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.0</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.4.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

认证服务代码

OAuth2配置

OAuth2使用简化模式,并且开启自动授权。简化模式需要用户先通过SpringSecurity认证。后面我们会配置SpringSecurity认证通过后自动访问OAuth2授权地址,因为开启了自动授权,所以会直接返回token给我们。

@Configuration
@EnableAuthorizationServer // 配置授权服务。
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    // 用来配置客户端详情服务
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 这里是第三方合作用户的客户id,秘钥的配置
        // 使用in-memory存储
        clients.inMemory()
                // 客户端账号
                .withClient("c1")
                // 客户端密钥
                .secret(new BCryptPasswordEncoder().encode("secret"))
                // 客户端可访问的资源列表
                .resourceIds("user")
                // 授权类型,此处使用简化模式
                .authorizedGrantTypes( "implicit","refresh_token")
                // 客戶端允许的授权范围,暂时用不上
                .scopes("all")
                // 是否自动授权,简化模式在登录SpringSecurity后还需要进行一个授权。false跳转到授权页面,让用户点击授权,如果是true,相当于自动点击授权,就不跳转授权页面
                .autoApprove(true)
                // 加上验证回调地址,返回授权码信息,授权码信息会附加在url后面
                .redirectUris("http://localhost:8083/auth/token.html");

        // 如果有多个用户,可以继续配置
        // .and().withClient()

    }

    /**
     * 使用JWT令牌
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("123");//JWT密码
        return converter;
    }

    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        //token增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter()));
        defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);

        defaultTokenServices.setAccessTokenValiditySeconds(43200);//令牌有效期12小时
        defaultTokenServices.setRefreshTokenValiditySeconds(259200);//刷新令牌有效期3天
        return defaultTokenServices;
    }

    @Autowired
    // 认证管理
    private AuthenticationManager authenticationManager;


    @Override
    // 用来配置令牌(token)的访问端点
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .authenticationManager(authenticationManager)
                // 令牌管理服务
                .tokenServices(tokenServices())
                .accessTokenConverter(accessTokenConverter())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);// 允许post提交
    }

    @Override
    // 用来配置令牌端点的安全约束,拦截规则
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    	//实际测试这三个不配置也可以,具体作用未知
        security
                // 提供公有密匙的端点,如果你使用JWT令牌的话, 允许
                .tokenKeyAccess("permitAll()")
                // oauth/check_token:用于资源服务访问的令牌解析端点,允许
                .checkTokenAccess("permitAll()")
                // 表单认证,申请令牌
                .allowFormAuthenticationForClients();
    }

}

SpringSecurity配置

SpringSecurity的配置和普通的单体应用一样,这里使用Filter+Manage+Provider的方式做登录,网上比较多的是用UserDetailsService。但本文是三种登录方式,用Filter+Manage+Provider方便实现。简单说下这种方式,Filter负责拦截请求,然后调用Manage,Manage负责选择合适的Provider处理该登录请求。具体请看这篇文章 SpringSecurity 多种登录方式

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    //注入三个provider,对应密码,手机号,邮箱三种登录方式
    @Bean
    PasswordAuthenticationProvider passwordAuthenticationProvider(){
        return new PasswordAuthenticationProvider();
    }
    @Bean
    PhoneAuthenticationProvider phoneAuthenticationProvider(){
        return new PhoneAuthenticationProvider();
    }
    @Bean
    EmailAuthenticationProvider emailAuthenticationProvider(){
        return new EmailAuthenticationProvider();
    }

    /**
     * 配置过滤器
     * @param authenticationManager
     * @return
     */
    AuthenticationProcessingFilter authenticationProcessingFilter(AuthenticationManager authenticationManager){
        AuthenticationProcessingFilter userPasswordAuthenticationProcessingFilter = new AuthenticationProcessingFilter();
        //为filter设置管理器
        userPasswordAuthenticationProcessingFilter.setAuthenticationManager(authenticationManager);
        //登录成功后跳转到OAuth2的认证地址
        userPasswordAuthenticationProcessingFilter.setAuthenticationSuccessHandler((httpServletRequest, httpServletResponse, authentication) -> {
            httpServletResponse.sendRedirect("http://localhost:8083/auth/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://localhost:8083/auth/token.html");
        });
        //登录失败后跳转
  userPasswordAuthenticationProcessingFilter.setAuthenticationFailureHandler((httpServletRequest, httpServletResponse, authentication) -> {
            httpServletResponse.sendRedirect("/login/failure");
        });
        return userPasswordAuthenticationProcessingFilter;
    }

    //配置管理器
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        //往管理器中添加provider
        auth.authenticationProvider(passwordAuthenticationProvider())
            .authenticationProvider(phoneAuthenticationProvider())
            .authenticationProvider(emailAuthenticationProvider());
    }

    // 认证管理器
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //注入密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        //return NoOpPasswordEncoder.getInstance(); // 不加密
        return new BCryptPasswordEncoder();// BCryptPasswordEncoder加密
    }

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

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    //配置拦截路径等信息
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .cors()
                .and()
                .authorizeRequests().antMatchers("/user/**","/test/**").authenticated()
                .and()
                .formLogin().loginPage("/login/noLogin");//未登录跳转到此接口
/*.loginProcessingUrl("/process")
                .successForwardUrl("/login/success")
                .failureForwardUrl("/login/failure");*/

            http.addFilterBefore(authenticationProcessingFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
    }
}

资源服务器代码

@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {

    /**
     * 配置JWT
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

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

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources
                // 资源id,与认证服务器对应,如果资源id不一致会认证失败
                .resourceId("user")
                //                // 令牌服务
                //                .tokenServices(tokenService())
                // 令牌服务
                .tokenStore(tokenStore())
                .stateless(true)
                //认证异常
                .authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
                    ObjectMapper objectMapper=new ObjectMapper();
                    httpServletResponse.setContentType("application/json;charset=UTF-8");
                    httpServletResponse.getWriter().write(objectMapper.writeValueAsString(R.error("认证异常")));
                })
                //无权访问
                .accessDeniedHandler((httpServletRequest, httpServletResponse, e) -> {
                    ObjectMapper objectMapper=new ObjectMapper();
                    httpServletResponse.setContentType("application/json;charset=UTF-8");
                    httpServletResponse.getWriter().write(objectMapper.writeValueAsString(R.error("无权访问")));
                });
    }

    /**
     * 配置拦截路径、权限等
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/user/**").hasRole("USER")
                .antMatchers("/admin/**").hasRole("ADMIN")
            // 所有的访问,授权访问都要是all,和认证服务器的授权范围一一对应
            //.access("#oauth2.hasScope('all')")
            //去掉防跨域攻击
            .and().csrf().disable();
            //session管理
            /*.sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);*/
    }

}

代码

github.com/cdhcrs/Spri…