leftso 8138 0 2018-09-02 16:21:11

引言

在本文中,我们将讨论有关Spring启动安全性和JWT令牌的OAUTH2实现以及保护REST API。在我上一篇Spring Boot Security OAUTH2示例中,我们使用OAUTH2创建了一个示例应用程序,用于使用默认令牌存储进行身份验证和授权,但Spring安全性OAUTH2实现还提供了定义自定义令牌存储的功能。在这里,我们将使用JwtTokenStore创建一个示例spring security OAUTH2应用程序。使用JwtTokenStore作为令牌提供程序允许我们自定义使用TokenEnhancer生成的令牌以添加其他声明。

    这个应用程序中的大部分配置与我以前的spring security OAUTH2实现文章非常相似,因此我们可能会避免在最后一个应用程序中构建的一些通用代码和配置。让我们从简单介绍JWT开始,然后我们将深入创建我们的授权服务器,资源服务器,稍后我们将讨论在令牌中添加自定义声明。
 

Json Web Token

      JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一个紧凑且独立的方式,用于在各方之间以JSON对象安全地传输信息。一种无状态身份验证机制,因为用户状态永远不会保存在服务器内存中。 JWT令牌由3个部分组成,用点(。)即Header.payload.signature分隔

头部包含2部分类型的令牌和散列算法。包含这两个键的JSON结构是Base64Encoded。
 

{
  "alg": "HS256",
  "typ": "JWT"
}

有效载荷包含claims。主要有三种类型的claims:保留,reserved, public, private。保留的claims是iss(发行人),exp(到期时间),sub(主题),aud(受众)等预定义claims。在private claims中,我们可以创建一些自定义claims,如主题,角色和其他。
{
  "sub": "Alex123",
  "scopes": [
    {
      "authority": "ROLE_ADMIN"
    }
  ],
  "iss": "http://devglan.com",
  "iat": 1508607322,
  "exp": 1508625322
}
签名确保令牌不会在途中发生变化。例如,如果您想使用HMAC SHA256算法,签名将按以下方式创建:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
以下是示例JWT令牌。这是一个带有jwt认证应用程序的完整堆栈spring boot程序,用于使用jwt令牌机制保护REST API。
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBbGV4MTIzIiwic2N.v9A80eU1VDo2Mm9UqN2FyEpyT79IUmhg
 

项目结构

spring  boot项目结构图

Maven的依赖

在这里,spring-security-jwt提供了对JwtTokenStore的支持。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.security.oauth</groupId>
	<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-jwt</artifactId>
</dependency>

授权服务器配置

我希望你熟悉OAUTH2架构和授权服务器。我在我上一篇文章OAUTH2中解释了它。下面的配置与我们上一次配置的Spring Boot Security OAUTH2实例非常相似,除了JwtAccessTokenConverter和TokenStore.Here,JwtAccessTokenConverter是在JWT编码的令牌值和OAuth之间转换的帮助器认证信息。我们添加了自定义签名以使JWT令牌更健壮。除了JwtTokenStore,spring安全性还提供了InMemoryTokenStore和JdbcTokenStore。

ClientDetailsServiceConfigurer:定义客户端详细信息服务的配置器。客户详细信息可以初始化,或者您可以参考现有的商店。

AuthorizationServerSecurityConfigurer :定义了令牌端点上的安全约束。

AuthorizationServerEndpointsConfigurer :定义授权和令牌端点以及令牌服务。

ClientDetailsS​​erviceConfigurer可用于定义客户端详细信息服务的内存中或JDBC实现。在此示例中,我们使用的是内存中的实现。

AuthorizationServerConfig.java
 

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

	static final String CLIEN_ID = "devglan-client";
	static final String CLIENT_SECRET = "devglan-secret";
	static final String GRANT_TYPE_PASSWORD = "password";
	static final String AUTHORIZATION_CODE = "authorization_code";
    static final String REFRESH_TOKEN = "refresh_token";
    static final String IMPLICIT = "implicit";
	static final String SCOPE_READ = "read";
	static final String SCOPE_WRITE = "write";
    static final String TRUST = "trust";
	static final int ACCESS_TOKEN_VALIDITY_SECONDS = 1*60*60;
    static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6*60*60;

	@Autowired
	private AuthenticationManager authenticationManager;

	@Bean
	public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("as466gf");
        return converter;
	}

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

	@Override
	public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {

		configurer
				.inMemory()
				.withClient(CLIEN_ID)
				.secret(CLIENT_SECRET)
				.authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT )
				.scopes(SCOPE_READ, SCOPE_WRITE, TRUST)
				.accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS).
				refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS);
	}

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.tokenStore(tokenStore())
				.authenticationManager(authenticationManager)
                .accessTokenConverter(accessTokenConverter());
	}
}

资源服务器配置

我们的上下文中的资源是我们为粗暴操作公开的REST API。要访问这些资源,必须对客户端进行身份验证。在实时场景中,每当用户尝试访问这些资源时,都会要求用户提供他真实性,一旦用户被授权,他将被允许访问这些受保护的资源。

resourceId: 资源的id(可选,但建议并且将由auth服务器验证,如果存在的话)。

因为我们在同一个项目中有资源服务器和服务器实现,所以我们不需要在资源服务器配置中重新定义我们的JwtAccessTokenConverter,否则我们需要在资源服务器中提供类似的JwtAccessTokenConverter实现。

在这里,我们已经配置/ users是一个受保护的资源,它需要一个ADMIN角色进行访问。
 

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

	private static final String RESOURCE_ID = "resource_id";
	
	@Override
	public void configure(ResourceServerSecurityConfigurer resources) {
		resources.resourceId(RESOURCE_ID).stateless(false);
	}

	@Override
	public void configure(HttpSecurity http) throws Exception {
        http.
                anonymous().disable()
                .authorizeRequests()
                .antMatchers("/users/**").access("hasRole('ADMIN')")
                .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
	}

}


 

REST API

现在让我们使用spring控制器公开一些受保护的REST资源。
 

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping(value="/user", method = RequestMethod.GET)
    public List listUser(){
        return userService.findAll();
    }

    @RequestMapping(value = "/user", method = RequestMethod.POST)
    public User create(@RequestBody User user){
        return userService.save(user);
    }

    @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)
    public String delete(@PathVariable(value = "id") Long id){
        userService.delete(id);
        return "success";
    }

}

以下是验证用户的用户服务实现。
 

@Service(value = "userService")
public class UserServiceImpl implements UserDetailsService, UserService {
	
	@Autowired
	private UserDao userDao;

	public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
		User user = userDao.findByUsername(userId);
		if(user == null){
			throw new UsernameNotFoundException("Invalid username or password.");
		}
		return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority());
	}

	private List getAuthority() {
		return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
	}

	public List findAll() {
		List list = new ArrayList<>();
		userDao.findAll().iterator().forEachRemaining(list::add);
		return list;
	}
}

以上用户服务在SecurityConfig.java中配置如下。您可以使用此在线Bcrypt计算器来生成Bcrypt密码。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource(name = "userService")
    private UserDetailsService userDetailsService;

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

    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(encoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .anonymous().disable()
                .authorizeRequests()
                .antMatchers("/api-docs/**").permitAll();
    }

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

    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(0);
        return bean;
    }
}


 

创建用户SQL脚本

INSERT INTO User (id, username, password, salary, age) VALUES (1, 'Alex123', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 3456, 33);
INSERT INTO User (id, username, password, salary, age) VALUES (2, 'Tom234', '$2a$04$PCIX2hYrve38M7eOcqAbCO9UqjYg7gfFNpKsinAxh99nms9e.8HwK', 7823, 23);
INSERT INTO User (id, username, password, salary, age) VALUES (3, 'Adam', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 4234, 45);

测试OAUTH2 JWT应用程序

首先,运行Application.java作为java程序并切换到POSTMAIN,并通过http:// localhost:8080 / oauth / token发出POST请求以生成标记。在标头中,我们选择了基本身份验证并提供了用户名和密码as devglan-clientdevglan-secret。这将导致access_token,token_type,refresh_token,过期等。

postmain请求
现在,我们可以使用相同的标记来访问受保护的资源。
访问受保护的资源
 

总结

在这里,我们讨论了如何使用JWT作为Spring启动安全性OAUTH2实现的令牌提供者。



项目源码下载