单点登录系列2-Spring-OAuth2项目搭建

6次阅读

共计 7519 个字符,预计需要花费 19 分钟才能阅读完成。

概述

在第一篇中,我们已经讲过了 OAuth2 单点登录的实际应用场景和技术方案,那么这一篇就具体讲解如何搭建 OAuth2 的服务。OAuth2 只是一个协议,实现该协议的技术产品有很多,比如:微软的 ADFS,Oracle 的 OAM(12c),等等。但这些产品都是大厂研发出来的,基本都是收费的,那么如果我们需要基于开源的技术,自己搭建基于 OAuth2 的服务该怎么做呢?你可以试试“Spring Cloud 全家桶”里面的 Spring Security OAuth2。

本文将讲解 Spring Security OAuth2 的项目实战搭建,由于篇幅有限,文章中只会摘录核心代码,完整代码请上 github 地址 查看。

最近看过一个非常复杂 Spring Security OAuth2 技术架构图,虽然很多功能点我自己也没有用到过,但是这里还是附上吧。

项目搭建

首先是创建一个 SpringBoot 项目,要在启动类加上 @EnableResourceServer 的注解。

pom.xml

   <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--oauth2-->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.0.15.RELEASE</version>
        </dependency>
        <!--freemarker, 自定义登录页使用 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <!--jwt,生成 jwt token-->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>1.0.9.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.6.0</version>
        </dependency>
        <!-- feign,非必需 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.0.2.RELEASE</version>
        </dependency>
    </dependencies>

核心的实现类主要只有两个,一个实现接口 WebSecurityConfigurerAdapter,另一个实现 AuthorizationServerConfigurerAdapter 接口。

WebSecurityConfigurerAdapter 主要用来定义 Web 请求的路由控制,比如:哪些路由受 security 控制;自定义登录页;登录成功或失败的处理;注销的处理,等等。包括还有 web.ignoring() 的方法,可以对指定 url 路径放行,不受单点登录控制。

WebSecurityCA.java

@Configuration
@Order(1)
public class WebSecurityCA extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthenticationFailureHandler appLoginFailureHandler;


    @Override
    protected void configure(HttpSecurity http) throws Exception {http.requestMatchers()
                .antMatchers("/login")
                .antMatchers("/oauth/authorize")
                .antMatchers("/oauth/token")
                .antMatchers("/logout")
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                // 自定义登录页面,这里配置了 loginPage, 就会通过 LoginController 的 login 接口加载登录页面
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .failureHandler(appLoginFailureHandler)
                .failureUrl("/login?error=true")
                // 注销
                .and()
                .logout()
                .addLogoutHandler(new MyLogoutHandler())
                .and()
                .csrf().disable();
    }

    /**
     * web ignore 比较适合配置前端相关的静态资源,它是完全绕过 spring security 的所有 filter 的
     * ingore 是完全绕过了 spring security 的所有 filter,相当于不走 spring security
     * permitall 没有绕过 spring security,其中包含了登录的以及匿名的
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/oauth/logout");
    }

    /**
     * 创建该实例,为了保证 密码模式中可以实现 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();
    }
}

AuthorizationServerConfigurerAdapter 接口则是实现 OAuth2 的核心代码,实现功能包括:开放 OAuth2 的验证模式;开放的 clientId 和 clientSecret;token 按照 jwt 协议生成;等等。

AuthorizationServerCA.java

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerCA extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private TokenStore tokenStore;
    @Autowired(required = false)
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Autowired(required = false)
    private TokenEnhancer jwtTokenEnhancer;
    @Autowired
    private WebResponseExceptionTranslator customWebResponseExceptionTranslator;
    @Autowired
    private OAuth2Properties oAuth2Properties;

    @Override
    public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {InMemoryClientDetailsServiceBuilder build = clients.inMemory();
            for (OAuth2ClientsProperties config : oAuth2Properties.getClients()) {build.withClient(config.getClientId())
                        .secret(passwordEncoder.encode(config.getClientSecret()))
                        .accessTokenValiditySeconds(config.getAccessTokenValiditySeconds())
                        .authorizedGrantTypes("refresh_token", "password", "authorization_code")//OAuth2 支持的验证模式
                        .scopes("user_info")
                        .autoApprove(true);
            }


    }


    /**
     * 密码 password 模式,需要实现该方法 authenticationManager
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.tokenStore(tokenStore)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);

        // 扩展 token 返回结果
        if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) {TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
            List<TokenEnhancer> enhancerList = new ArrayList();
            enhancerList.add(jwtTokenEnhancer);
            enhancerList.add(jwtAccessTokenConverter);
            tokenEnhancerChain.setTokenEnhancers(enhancerList);
            //jwt
            endpoints.tokenEnhancer(tokenEnhancerChain)
                    .accessTokenConverter(jwtAccessTokenConverter);
        }
        endpoints.exceptionTranslator(customWebResponseExceptionTranslator);
    }
}

自定义模块

1、自定义登录 / 注销

1.1、登录页

首先自定义开发一个登录页,并开发接口保证访问 url 能访问到登录页,例如:/login。

其次在 WebSecurityConfigurerAdapter 实现类的 configure(HttpSecurity http) 方法中,指明自定义登录页页路径 .formLogin().loginPage(“/login”)

1.2、注销

在登录成功后会生成认证通过的 cookie,保证下次跳转到登录页时无需登录就能通过。而注销的操作就是清除该 cookie,Spring OAuth2 默认的注销地址是:/logout,并且注销成功后会自动重定向到登录页。

修改方式同样也是在 WebSecurityConfigurerAdapter 实现类的 configure(HttpSecurity http) 方法中,.logout().addLogoutHandler(new MyLogoutHandler()) 方法可以自定义注销的实现逻辑,例如 MyLogoutHandler() 就是我自己实现的处理逻辑,注销成功后会跳转到上一页。

MyLogoutHandler.java

@Component
public class MyLogoutHandler implements LogoutHandler {
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        try {final String refererUrl = request.getHeader("Referer");
            response.sendRedirect(refererUrl);// 实现自定义重定向
        } catch (IOException e) {e.printStackTrace();
        }
    }
}

2、自定义 token

Spring OAuth2 在登录成功后会生成 access_token 和 refresh_token,但这些 token 默认是类似于 uuid 的字符串,我们怎么把他们换成 jwt 的 token 呢?

在之前 AuthorizationServerCA.java 类中我们能看到使用 jwt 方式发放 token 的配置,包括其中有用到自定义的 JwtTokenEnhancer 类,可以通过.setAdditionalInformation 拓展更多的自定义参数。

public class JwtTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {Map<String, Object> info = new HashMap<>();
        info.put("name","吴晨瑞");
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
        return accessToken;
    }
}

3、自定义用户验证

在用户输入用户名和密码后,校验是否正常的程序在哪里定义呢?我们一般要自定义类来实现 UserDetailsService 接口。这个接口里面只有一个方法 loadUserByUsername(String username),传入参数是 用户名,你可以自定义方法获取数据库中该用户名对应的密码,然后 Spring Auth2 服务会将你数据库中获取的密码和页面上输入的密码比对,判断你是否登录成功。

MyUserDetailsService.java

@Component
public class MyUserDetailsService implements UserDetailsService {
@Resource
private UserFeign userFeign;

    @Override
    public UserDetails loadUserByUsername(String username) throws BadCredentialsException {
        //enable:用户已失效
        //accountNonExpired:用户帐号已过期
        //credentialsNonExpired:坏的凭证
        //accountNonLocked:用户账号已锁定
        // return new User("dd", "1", true, true, false, true, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
        String password= userFeign.loadUserByUsername(username);
        return new User(username, passwordEncoder().encode(password), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }

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

4、其他

还有一些自定义的登录异常处理、权限异常处理,这里就不一一附上了,可以在 github 上参考相关代码。Spring OAuth2 有自己一套非常完整的体系,各个接口都可以自定义实现,就像文章开头我附上的那张图一样。如果各位看客感兴趣并且有时间,可以一一实习这些接口,打造一个自己 OAuth2 单点登录系统。

正文完
 0