前言

由于公司要求使用Spring-Security-Oauth2快速搭建一套账号中心,此版本为初级版本,仅为作为记录学习下Spring-Security-Oauth2的原理和快速搭建。

开发前准备

在快速搭建之前首先要弄明白Oauth2是什么,大概的运行流程是怎么样子的,而Security是一套Spirng提供的一套安全框架,这个没有接触到的小伙伴可以在这里停住了,由于我已经大概了解是怎么用的,这里就不再过多逼逼。

Oauth简单理解

首先呢Oauth2是一种关于授权的开放网络标准,在讲解运行流程前需要了解几个专用名词

Third-party application:第三方应用程序

HTTP service:HTTP服务提供商

Resource Owner:资源所有者(用户)

User Agent:用户代理(浏览器)

Authorization server:认证服务器

Resource server:资源服务器

来了解上面的专业名词后,来上原理图,好的这就来~

springsecurity和 oauth2和 jwt springsecurity oauth2原理_java

(A) 用户打开client(客户端)以后,客户端要求Resource Owner(用户)给予授权

(B) Resource Owner(用户)统一授权

© client(客户端)使用上一步获得的授权,向认证服务器申请令牌。

(D) Authorization server(认证服务器)对client(客户端)进行认证以后,确认无误,同意发放令牌。

(E) client(客户端)使用令牌,向Resouce Server(资源服务器)申请获取资源。

(F) Resouce Server(资源服务器)确认令牌无误,同意向client(客户端)开放资源。

简单来讲就是,客户端先拿到用户授权,授权后,客户端就可以向认证服务器获取令牌,在通过令牌访问到资源服务器。这里只是简单的描述,有点抽象,需要更详细的了解可以参照理解OAuth 2.0 - 阮一峰.

Oauth2定义了4中授权模式

  • 授权码模式
  • 简化模式
  • 密码模式
  • 客户端模式

本文主要讲解的是密码模式哦!



项目搭建

主要依赖如下:

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Spring Security和JWT整合 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.10.RELEASE</version>
</dependency>

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>${security-oauth2.version}</version>
</dependency>

配置Spring-Security

/**
 * 安全认证配置,校验账号密码
 *
 * @author ikcross
 */
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    /**
     * 密码加密方式
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager manager = super.authenticationManagerBean();
        return manager;
    }

    /**
     * 拦截规则
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 允许所有用户访问"/"和"/index.html"
        http.requestMatchers().anyRequest()
                .and()
                .authorizeRequests()
                .antMatchers("/oauth/**").permitAll();
    }

    /**
     * 验证
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户
        // PasswordEncoder 用于密码的加密与比对
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}

我们在认证之前,首先要校验用户的账号密码是否正确,在configure(AuthenticationManagerBuilder auth)里面,配置了userDetailsService,这个里面的实现类就是在内存中配置了用户的账号信息,详细可以参考下面的userDetailsService方法,注意这里使用了BCryptPasswordEncoder()进行加密,在Spring2.x中不加密会报错的哦

配置认证服务器

/**
 * 认证服务器
 *
 * @author ikcross
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;


    @Bean(name = "userDetailsServiceImpl")
    public UserDetailsService userDetailsService() {
        return new UserDetailsServiceImpl();
    }

    @Bean(name = "clientDetailsServiceImpl")
    public ClientDetailsService clientDetailsService() {
        return new ClientDetailsServiceImpl();
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients()
                // 开启/oauth/token_key验证端口无权限访问
                .tokenKeyAccess("permitAll()")
                // 开启/oauth/check_token验证端口认证权限访问
                .checkTokenAccess("isAuthenticated()");
    }

    /**
     * 用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService());
    }

    /**
     * 用来配置令牌端点(Token Endpoint)的安全约束
     * <p>
     * .authenticationManager:密码模式下配置认证管理器 AuthenticationManager
     * .userDetailsService:
     * .tokenServices:token保存方式
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService())
                .tokenStore(new RedisTokenStore(redisConnectionFactory));
    }
}

来到认证服务器,说明用户验证已经完成,这个configure(ClientDetailsServiceConfigurer clients)方法里面配置了clientDetailsService方法,里面配置了需要认证客户端的信息,具体信息在下面配置的clientDetailsService方法在解释吧!

配置资源服务器

/**
 * 资源服务器
 *
 * @author ikcross
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("rId1").stateless(false);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
//        所有请求都必须登陆才能访问
//        http.authorizeRequests().anyRequest().authenticated();

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                .requestMatchers().anyRequest()
                .and()
                .anonymous()
                .and()
                .authorizeRequests()
                //配置order访问控制,必须认证过后才可以访问
                .antMatchers("/order/**").authenticated();

    }

配置userDetailsServiceImpl和clientDetailsServiceImpl

/**
 * 客户端详情实现类
 *
 * @author ikcross
 */
public class ClientDetailsServiceImpl implements ClientDetailsService {

    private static final Set<String> RESOURCE_IDS = Sets.newHashSet("rId1");

    private static final Set<String> AUTHORIZED_GRANT_TYPES = Sets.newHashSet("password", "refresh_token");

    private static final Set<String> SCOPES = Sets.newHashSet("select");

    private static final List<GrantedAuthority> AUTHORITIES = AuthorityUtils.createAuthorityList("client");

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {

        // 写死客户端信息,后续可配置到数据库
        BaseClientDetails clientDetails = new BaseClientDetails();
        clientDetails.setClientId("client_2");
        clientDetails.setResourceIds(RESOURCE_IDS);
        clientDetails.setAuthorizedGrantTypes(AUTHORIZED_GRANT_TYPES);
        clientDetails.setScope(SCOPES);
        clientDetails.setAuthorities(AUTHORITIES);
        clientDetails.setClientSecret(passwordEncoder.encode("123456"));
        return clientDetails;
    }
}

详细的说一下

client_id代表是那个客户端也就是那个App或者Web服务需要认证,

resourceId代表可以访问哪个resourceId

setAuthorizedGrantTypes代表授权类型,这里我用的是密码模式

scopes代表范围

setClientSecret客户端秘钥就是密码

/**
 * 用户详情实现类
 *
 * @author ikcross
 */
public class UserDetailsServiceImpl implements UserDetailsService {
    private static final Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    private static final List<GrantedAuthority> AUTHORITIES = AuthorityUtils.createAuthorityList("USER");

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("UserDetailsServiceImpl.loadUserByUsername");

        return User.withUsername("user_2")
                .password(passwordEncoder.encode("123456"))
                .authorities(AUTHORITIES)
                .build();
    }
}

loadUserByUsername字面意思,通过username获取用户信息,这里写死了账号密码权限

提供外部接口来测试

/**
 * 测试用Controller,提供外部接口测试
 *
 * @author ikcross
 */
@RestController
public class TestController {
    /**
     * 商品查询接口:不做安全访问空控制
     * @param id
     * @return
     */
    @RequestMapping("/product/{id}")
    public String getProduct(String id) {
        //for debug
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return "product id : " + id;
    }

    /**
     * 订单查询接口,后续添加访问控制
     * @param id
     * @return
     */
    @GetMapping("/order/{id}")
    public String getOrder(@PathVariable String id) {
        //for debug
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return "order id : " + id;
    }

}

配完上面的就可以启动项目咯,好耶,终于可以启动了

测试验证

首先是看下获取token:/oauth/token

springsecurity和 oauth2和 jwt springsecurity oauth2原理_客户端_02

响应:

springsecurity和 oauth2和 jwt springsecurity oauth2原理_oauth2_03

再配置中,我们做了/order/**的资源保护,如果直接访问

springsecurity和 oauth2和 jwt springsecurity oauth2原理_spring_04

/product/**没有做任何限制,可以直接访问

springsecurity和 oauth2和 jwt springsecurity oauth2原理_客户端_05

带上token就可以访问了,一切完美

springsecurity和 oauth2和 jwt springsecurity oauth2原理_java_06

以上,一个简单的Spring-Security-Oauth2项目就搭建好了,这只是怎么搭建,还有token的工作原理,怎么创建的,里面有什么信息,内部是如何验证的,等等,如果有时间,我会继续写到文章记录下来。

以上示例的代码可能不完整,完整的初级版本的代码我会放到github上面哦。如果需要可以下载来看下!

启动的话记得启用redis!