Sometimes we only need a simple token based security framework to protect the rest API. Obviously we like Spring Framework but Spring Security is too heavy to us. It's possible to extends the Spring Security to match our requirement, but that means we have to pay the cost for the over-designed filter chain at the runtime. Daas-Token is designed to reduce down the filters in the chain ,abandon the complicated structure , and try to make it simple , make it easy to understand.
We are still the heavy users of Spring Framework , so when we decide to develop the Daas-Token security framework , we only consider to support the Spring Framework without any hesitation.
Thanks Spring Security. There are some useful APIs available for us in Spring Security , but we don't like to depend on the fully JARS , so we re-package them under the following package.
in.clouthink.daas.security.token.repackage.org.springframework.security
- Spring Framework 3.2.x (Core & Web Required)
- Spring Data Redis (Optional)
- Spring Data Mongodb (Optional)
- Spymemcached (Optional)
So far the following version is available
| module name | latest version | features |
|---|---|---|
| daas-token | 1.8.1 | captcha |
| daas-token | 1.7.0 | login attempt |
| daas-token | 1.6.0 | feature configurer |
| daas-token | 1.5.2 | compatible with spring-boot |
| daas-token | 1.5.0 | authn & authz event |
| daas-token | 1.4.6 | federation authn |
<dependency>
<groupId>in.clouthink.daas</groupId>
<artifactId>daas-token</artifactId>
<version>${daas.token.version}</version>
</dependency> compile "in.clouthink.daas:daas-token:${daas_token_version}"AuthenticationFeature is designed to control the authentication behaviour. Here are the available features.
| feature | default value | description |
|---|---|---|
| CORS_SUPPORT | false | CORS is not support by default, please enable it if required. |
| STRICT_TOKEN | true | The token must be supplied in http header. |
| IGNORE_PRE_AUTHN_ERROR | false | When the request not pass the PreAuthenticationFilter authentication, continue the filter chain if set it to false. |
| LOGIN_ATTEMPT_ENABLED | false | If the user attempts login and goes failure to the defined max times, the user will be locked. |
| CAPTCHA_ENABLED | false | If enabled, the user must pass the captcha verify while login |
| POST_LOGIN_ONLY | true | Only the HTTP POST login is allowed if enabled. |
Here is the sample to enable or disable the features
@Bean
public TokenConfigurer tokenConfigurer() {
return new TokenConfigurerAdapter() {
@Override
public void configure(FeatureConfigurer featureConfigurer) {
featureConfigurer.enable(AuthenticationFeature.LOGIN_ATTEMPT_ENABLED);
}
}
}Use @EnableToken to get started
@Configuration
@EnableToken
public class Application {}By default, the JVM memory-based token management is working for Daas-Token, @Scheduled(cron = "0 0/10 * * * ?") is triggered every 10 minutes to clean up the expired token & login attempts.
So please enable the spring schedule feature if you does not change the default configuration.Otherwise , out of memory should be a big problem.
We recommend to use Redis to replace the default memory-based store to manage the token and login attempts.
@Configuration
@EnableScheduling
@EnableToken
public class Application {}The Daas-Token is working well with SpringBootApplication and please exclude the SecurityAutoConfiguration.
@SpringBootApplication
@EnableAutoConfiguration(exclude = SecurityAutoConfiguration.class)
@EnableToken
public class Application {}To customize your Daas-Token settings, please implement the interface TokenConfigurer .
public interface TokenConfigurer {
void configure(MessageProvider messageProvider);
void configure(TokenAuthenticationFilter filter);
void configure(AuthorizationFilter filter);
void configure(AuthenticationFilter filter);
void configure(LoginEndpoint endpoint);
void configure(LogoutEndpoint endpoint);
void configure(UrlAclProviderBuilder builder);
void configure(FeatureConfigurer featureConfigurer);
void configure(TokenOptions tokenOptions);
void configure(LoginAttemptOptions loginAttemptOptions);
void configure(CaptchaOptions captchaOptions);
}Here is the sample
@Bean
public TokenConfigurer myTokenConfigurer() {
//TODO:create and return the TokenConfigurer instance here.
}Or you can extend the adapter class TokenConfigurerAdapter which supplies the dummy implementation for TokenConfigurer , just override the methods you'd like to.
For example , we'd like to let the token only be alive for one hour.
@Bean
public TokenConfigurer myTokenConfigurer() {
return new TokenConfigurerAdapter() {
@Override
public void configure(TokenLife tokenLife) {
tokenLife.setTokenTimeout(60 * 60 * 1000);
}
}
}
- LoginEndpoint
Set the login process url:
@Bean
public TokenConfigurer myTokenConfigurer() {
return new TokenConfigurerAdapter() {
@Override
public void configure(LoginEndpoint endpoint) {
endpoint.setLoginProcessesUrl("/login");
}
}
}
- LogoutEndpoint
Set the logout process url:
@Bean
public TokenConfigurer myTokenConfigurer() {
return new TokenConfigurerAdapter() {
@Override
public void configure(LogoutEndpoint endpoint) {
endpoint.setLogoutProcessesUrl("/logout");
}
}
}
- AuthenticationFilter
- AuthorizationFilter
Set the protected rest url:
@Bean
public TokenConfigurer myTokenConfigurer() {
return new TokenConfigurerAdapter() {
@Override
public void configure(AuthenticationFilter filter) {
filter.setProcessesUrl("/protected**");
}
@Override
public void configure(AuthorizationFilter filter) {
filter.setProcessesUrl("/protected**");
}
}
}The AuthenticationFilter is responsible to judge whether is user is authenticated or not.
And the AuthorizationFilter is responsible to decide whether the user is allowed to access the protected url.
Normally, keep the process url of the two filters be the same value.
Now here goes to the step to define the ACL which is designed as RBAC , but not only RBAC. Url Acl is designed into three parts
- URL
The http url (support regex and ant path) - Http Method
the http method (support GET , PUT, POST and DELETE) - Grant Rule
the user access is granted if the request is matching the rules
The Grant Rule is defined as expression, now we support the following two format :
- ROLE:
XXX
XXX should be the role name (Role#getName()) - USERNAME:
XXX
XXX should be the user name (User#getUsername())
For example: The user which's username is TESTUSER or owns the Role TEST can access the /token/sample/helloworld with the http GET method
@Bean
public TokenConfigurer myTokenConfigurer() {
return new TokenConfigurerAdapter() {
@Override
public void configure(UrlAclProviderBuilder builder) {
builder.add(UrlAclBuilder.antPathBuilder()
.url("/token/sample/helloworld")
.httpMethods(HttpMethod.GET)
.grantRules("ROLE:TEST,USERNAME:TESTUSER"));
}
}
}Enable the LOGIN_ATTEMPT_ENABLED feature and customize the LoginAttemptOptions for login attempt.
public void configure(FeatureConfigurer featureConfigurer) {
featureConfigurer.enable(AuthenticationFeature.LOGIN_ATTEMPT_ENABLED);
}
public void configure(LoginAttemptOptions loginAttemptOptions) {
loginAttemptOptions.setMaxAttempts((short) 3);
}
LoginAttemptOptions Refz
| options | default value | desc |
|---|---|---|
| maxAttempts | 5 | The max attempts count, once equals or greater than it , the user will be locked. |
| attemptTimeout | 24 * 60 * 60 * 1000 | milli seconds , the user login attempts failure count duration ,once equals or greater than it , it will be reset or clear. |
Enable the CAPTCHA_ENABLED feature and customize CaptchaOptions for captcha options.
public void configure(FeatureConfigurer featureConfigurer) {
featureConfigurer.enable(AuthenticationFeature.CAPTCHA_ENABLED);
}
public void configure(CaptchaOptions captchaOptions) {
captchaOptions.setLength(6);
captchaOptions.setCaptchaTimeout(60 * 1000);
}
CaptchaOptions Refz
| options | default value | desc |
|---|---|---|
| length | 4 | length the length of the captcha, must be the range of 4~12 |
| numberEnabled | true | number including in the generated captcha |
| charEnabled | true | char including in the generated captcha |
| caseSensitive | false | the captcha is case-sensitive or not |
| captchaTimeout | 30 * 1000 | timeout milli seconds , must be the range of 30s ~ 10*60s |
As mentioned before, the JVM memory based token management is used by default, but we supplied the redis based token management, here is the way to enable the feature.
First, enabled the spring data redis feature as follow:
The
spring-data-redismakesRedisTemplateeasier. Please skip the below source if you are working withspring-boot&&spring-data-redis.
@Value("${redis.host}")
private String redisHost;
@Value("${redis.port}")
private int redisPort;
@Bean
public RedisConnectionFactory jedisConnectionFactory() {
RedisConnectionFactory result = new JedisConnectionFactory(new JedisShardInfo(redisHost,
redisPort));
return result;
}
@Bean
public RedisTemplate redisTemplate() {
RedisTemplate result = new RedisTemplate();
result.setConnectionFactory(jedisConnectionFactory());
result.setKeySerializer(new StringRedisSerializer(Charset.forName("UTF-8")));
return result;
}Then create the bean in.clouthink.daas.security.token.spi.impl.redis.TokenProviderRedisImpl.Please remember to add @Primary annotation with @Bean,
it will take the place of the default implementation
@Primary
@Bean
public TokenProvider redisTokenProvider() {
return new TokenProviderRedisImpl();
}Or replace the default LoginAttemptProviderMemoryImpl with Redis impl.
@Primary
@Bean
public LoginAttemptProvider redisLoginAttemptProvider() {
return new LoginAttemptProviderRedisImpl();
}Same as the way of redis token management, using memcached as the token store is easy to configure. We use the https://github.com/couchbase/spymemcached as the memcached java client.
@Value("${memcached.host}")
private String memcachedHost;
@Value("${memcached.port}")
private int memcachedPort;
@Bean
public MemcachedClientFactoryBean memcachedClientFactoryBean() {
MemcachedClientFactoryBean result = new MemcachedClientFactoryBean();
result.setServers(memcachedHost + ":" + memcachedPort);
return result;
}Then create the bean in.clouthink.daas.security.token.spi.impl.memcached.TokenProviderMemcachedImpl.Please remember to add @Primary annotation with @Bean,
it will take the place of the default implementation
@Primary
@Bean
public TokenProvider memcachedTokenProvider() {
return new TokenProviderMemcachedImpl();
}Or replace the default LoginAttemptProviderMemoryImpl with Memcached impl.
@Primary
@Bean
public LoginAttemptProvider memcachedLoginAttemptProvider() {
return new LoginAttemptProviderMemcachedImpl();
}Mongodb is one of the most popular nosql data store , we support to save the token back to mongodb , here is the configuration
The
spring-data-mongodbmakesMongoTemplateeasier. Please skip the below source if you are working withspring-boot&&spring-data-mongodb.
@Value("${mongodb.host}")
private String mongodbHost;
@Value("${mongodb.port}")
private int mongodbPort;
@Value("${mongodb.database}")
private String mongodbDatabase;
@Bean
public MongoDbFactory mongoDbFactory() throws Exception {
return new SimpleMongoDbFactory(new MongoClient(mongodbHost,
mongodbPort),
mongodbDatabase);
}
@Bean
public MongoTemplate mongoTemplate() throws Exception {
return new MongoTemplate(mongoDbFactory());
}Then create the bean in.clouthink.daas.security.token.spi.impl.mongodb.TokenProviderMongodbImpl.Please remember to add @Primary annotation with @Bean,
it will take the place of the default implementation
@Primary
@Bean
public TokenProvider mongodbTokenProvider1() {
return new TokenProviderMongodbImpl();
}Or replace the default LoginAttemptProviderMemoryImpl with Mongodb impl.
@Primary
@Bean
public LoginAttemptProvider mongodbLoginAttemptProvider() {
return new LoginAttemptProviderMongodbImpl();
}Redis is good choose to make the data cache-able and persist-able, but you can composite the memcached and mongodb together to achieve this target.
It's very easy to configure DaaS-Token to support this feature.Create the bean in.clouthink.daas.security.token.spi.impl.CompositeTokenProvider and
add @Primary annotation to the compositeTokenProvider.
@Bean
public TokenProvider memcachedTokenProvider() {
return new TokenProviderMemcachedImpl();
}
@Bean
public TokenProvider mongodbTokenProvider1() {
return new TokenProviderMongodbImpl();
}
@Primary
@Bean
public TokenProvider compositeTokenProvider() {
return new CompositeTokenProvider(memcachedTokenProvider(), mongodbTokenProvider1());
}Daas-Token is one light-weighted security framework but it doesn't mean we will sacrifice the performance to exchange the simple usage. The SPI is available for the advanced user to adapt their own implementation, even we have supported the Memcached, Redis and Mongodb out of the box.
Just supply your implementation
public interface TokenProvider<T extends Token> {
public void saveToken(T token);
public T findByToken(String token);
public void revokeToken(T token);
}Maybe you'd like to save the access control list back to the data store , and want to authorize the access request based on the dynamic data not hard-coded configuration. The SPI supplies the extension point if you want customize the authorization behaviors.
public interface AclProvider<T extends Acl> {
public List<T> listAll();
}
public interface AccessRequestVoter<T extends AccessRequest> {
public AccessResponse vote(T t, String grantRule);
}Please refer to the default implementations by DaaS-Token
- in.clouthink.daas.security.token.spi.impl.DefaultUrlAclProvider
- in.clouthink.daas.security.token.core.acl.AccessRequestRoleVoter
- in.clouthink.daas.security.token.core.acl.AccessRequestUserVoter
Once the user passed the authentication , the token response is sent back to the user. And then you can access the protected url resource with the token in the http header.
MultiValueMap<String, String> bodyMap = new LinkedMultiValueMap<String, String>();
bodyMap.add("username", "your username");
bodyMap.add("password", "your password");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(bodyMap,
headers);
Map result = new RestTemplate().postForObject("http://127.0.0.1/login",
request,
Map.class);
String token = (String) ((Map) result.get("data")).get("token");
HttpHeaders headers = new HttpHeaders();
String bearer = new String(Base64.encode(token.getBytes("UTF-8")),
"UTF-8");
headers.set("Authorization", "Bearer " + bearer);
HttpEntity request = new HttpEntity(headers);
ResponseEntity<String> result = new RestTemplate().exchange("http://127.0.0.1/token/sample",
HttpMethod.GET,
request,
String.class); Error response format (JSON) for example:
{"message":"The token is disabled","succeed":false,"errorCode":"error.tokenIsDisabled"}Explain
| error code | error message | http status code | description |
|---|---|---|---|
| error.invalidUserOrPassword | Invalid username or password. | 200 | |
| error.invalidTokenOrExpired | The session is invalid or expired. | 401 | |
| error.tokenIsDisabled | The token is disabled. | 401 | |
| error.userIsLocked | The user is locked. | 200 | |
| error.userIsDisabled | The user is disabled. | 200 | |
| error.userIsExpired | The user is expired. | 200 | |
| error.authenticationRequired | Authentication required. | 401 | |
| error.authenticationFailed | Authentication failed. | 401 | |
| error.authorizationFailed | Authorization failed. | 401 | |
| error.noPermission | No permission.Access denied. | 403 | |
| error.loginAttemptFailure | Wrong password. In %d hours only %d time(s) left. | 200 | since 1.7.0 |
| error.loginLocked | Wrong %d times. User is locked. | 200 | since 1.7.0 |
| error.incorrectCaptcha | Incorrect captcha. | 200 | since 1.8.0 |
| error.captchaIsExpired | Captcha is not found or expired. | 200 | since 1.8.0 |
<filter>
<filter-name>daasTokenLoginEndpoint</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>daasTokenLoginEndpoint</param-value>
</init-param>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter>
<filter-name>daasTokenLogoutEndpoint</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>daasTokenLogoutEndpoint</param-value>
</init-param>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter>
<filter-name>daasTokenPreAuthenticationFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>daasTokenPreAuthenticationFilter</param-value>
</init-param>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter>
<filter-name>daasTokenAuthenticationFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>daasTokenAuthenticationFilter</param-value>
</init-param>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter>
<filter-name>daasTokenAuthorizationFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>daasTokenAuthorizationFilter</param-value>
</init-param>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>daasTokenLoginEndpoint</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>daasTokenLogoutEndpoint</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>daasTokenPreAuthenticationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>daasTokenAuthenticationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>daasTokenAuthorizationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>