Implemented AJAX based authentication
This commit is contained in:
parent
5bacd10112
commit
b12f1a1137
83
etc/blog.md
Normal file
83
etc/blog.md
Normal file
@ -0,0 +1,83 @@
|
||||
## Table of contents:
|
||||
1. <a title="Introduction: JWT Token" href="#introduction">Introduction</a>
|
||||
2. <a title="Ajax authentication" id="ajax-authentication">Ajax authentication</a>
|
||||
|
||||
### <a name="introduction" id="introduction">Introduction</a>
|
||||
|
||||
Following are three scenarios that will be implemented in this tutorial:
|
||||
1. Ajax Authentication
|
||||
2. JWT Token
|
||||
3. URL Based Authentication with JWT Token
|
||||
|
||||
### Prerequisites
|
||||
|
||||
First step is to create empty Spring Boot project. Visit spring initializr website(https://start.spring.io/) to generate boilerplate.
|
||||
|
||||
Lets start by creating base package structure for our sample code.
|
||||
|
||||
```
|
||||
+---main
|
||||
| +---java
|
||||
| | +---com
|
||||
| | | \---svlada
|
||||
| | | +---common
|
||||
| | | \---security
|
||||
| | | +---auth
|
||||
| | | | +---ajax
|
||||
| | | | \---jwt
|
||||
| | | +---config
|
||||
| | | +---exceptions
|
||||
| | | \---model
|
||||
| \---resources
|
||||
| +---static
|
||||
| \---templates
|
||||
\---test
|
||||
\---java
|
||||
\---com
|
||||
\---svlada
|
||||
```
|
||||
|
||||
### <a name="ajax-authentication" id="ajax-authentication">Ajax authentication</a>
|
||||
|
||||
Code for ajax authentication will reside in the following package: com/svlada/security/auth/ajax.
|
||||
|
||||
In order to implement Ajax Login in Spring Boot we'll need to implement a couple of components.
|
||||
|
||||
1. AjaxLoginProcessingFilter
|
||||
2. AjaxAuthenticationProvider
|
||||
3. AjaxAwareAuthenticationSuccessHandler
|
||||
4. AjaxAwareAuthenticationFailureHandler
|
||||
5. RestAuthenticationEntryPoint
|
||||
6. WebSecurityConfig
|
||||
|
||||
Let's dive in the implementation details.
|
||||
|
||||
#### AjaxLoginProcessingFilter
|
||||
|
||||
#### Security Config
|
||||
|
||||
Create WebSecurityConfig class and put it in the com.svlada.security.config package.
|
||||
|
||||
WebSecurityConfig class needs to extend org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter.
|
||||
|
||||
#### Un-successufull access to protected resource
|
||||
|
||||
Request
|
||||
```
|
||||
GET /api/me HTTP/1.1
|
||||
Host: localhost:9966
|
||||
Cache-Control: no-cache
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"timestamp": 1470301809962,
|
||||
"status": 401,
|
||||
"error": "Unauthorized",
|
||||
"message": "Full authentication is required to access this resource",
|
||||
"path": "/api/me"
|
||||
}
|
||||
```
|
||||
|
||||
#### Successufull ajax authentication
|
||||
33
pom.xml
33
pom.xml
@ -22,6 +22,7 @@
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
<jjwt.version>0.6.0</jjwt.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@ -33,12 +34,44 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.3.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>joda-time</groupId>
|
||||
<artifactId>joda-time</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -2,10 +2,18 @@ package com.svlada;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Sample application for demonstrating security with JWT Tokens
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 3, 2016
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableConfigurationProperties
|
||||
public class SpringbootSecurityJwtApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SpringbootSecurityJwtApplication.class, args);
|
||||
}
|
||||
|
||||
27
src/main/java/com/svlada/common/ErrorCode.java
Normal file
27
src/main/java/com/svlada/common/ErrorCode.java
Normal file
@ -0,0 +1,27 @@
|
||||
package com.svlada.common;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
/**
|
||||
* Enumeration of REST Error types.
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 3, 2016
|
||||
*/
|
||||
public enum ErrorCode {
|
||||
GLOBAL(2),
|
||||
|
||||
AUTHENTICATION(10), JWT_TOKEN_EXPIRED(11);
|
||||
|
||||
private int errorCode;
|
||||
|
||||
private ErrorCode(int errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public int getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
52
src/main/java/com/svlada/common/ErrorResponse.java
Normal file
52
src/main/java/com/svlada/common/ErrorResponse.java
Normal file
@ -0,0 +1,52 @@
|
||||
package com.svlada.common;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* Error model for interacting with client.
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 3, 2016
|
||||
*/
|
||||
public class ErrorResponse {
|
||||
// HTTP Response Status Code
|
||||
private final HttpStatus status;
|
||||
|
||||
// General Error message
|
||||
private final String message;
|
||||
|
||||
// Error code
|
||||
private final ErrorCode errorCode;
|
||||
|
||||
private final Date timestamp;
|
||||
|
||||
protected ErrorResponse(final String message, final ErrorCode errorCode, HttpStatus status) {
|
||||
this.message = message;
|
||||
this.errorCode = errorCode;
|
||||
this.status = status;
|
||||
this.timestamp = new java.util.Date();
|
||||
}
|
||||
|
||||
public static ErrorResponse of(final String message, final ErrorCode errorCode, HttpStatus status) {
|
||||
return new ErrorResponse(message, errorCode, status);
|
||||
}
|
||||
|
||||
public Integer getStatus() {
|
||||
return status.value();
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public ErrorCode getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public Date getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
31
src/main/java/com/svlada/common/WebUtil.java
Normal file
31
src/main/java/com/svlada/common/WebUtil.java
Normal file
@ -0,0 +1,31 @@
|
||||
package com.svlada.common;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 3, 2016
|
||||
*/
|
||||
public class WebUtil {
|
||||
private static final String XML_HTTP_REQUEST = "XMLHttpRequest";
|
||||
private static final String X_REQUESTED_WITH = "X-Requested-With";
|
||||
|
||||
private static final String CONTENT_TYPE = "Content-type";
|
||||
private static final String CONTENT_TYPE_JSON = "application/json";
|
||||
|
||||
public static boolean isAjax(HttpServletRequest request) {
|
||||
return XML_HTTP_REQUEST.equals(request.getHeader(X_REQUESTED_WITH));
|
||||
}
|
||||
|
||||
public static boolean isAjax(SavedRequest request) {
|
||||
return request.getHeaderValues(X_REQUESTED_WITH).contains(XML_HTTP_REQUEST);
|
||||
}
|
||||
|
||||
public static boolean isContentTypeJson(SavedRequest request) {
|
||||
return request.getHeaderValues(CONTENT_TYPE).contains(CONTENT_TYPE_JSON);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.svlada.profile.endpoint;
|
||||
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.svlada.security.model.UserContext;
|
||||
|
||||
/**
|
||||
* End-point for retrieving logged-in user details.
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 4, 2016
|
||||
*/
|
||||
@RestController
|
||||
public class ProfileEndpoint {
|
||||
@RequestMapping(value="/api/me", method=RequestMethod.GET)
|
||||
public @ResponseBody UserContext get() {
|
||||
throw new NotImplementedException("Not implemented");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.svlada.security;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 4, 2016
|
||||
*/
|
||||
@Component
|
||||
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex)
|
||||
throws IOException, ServletException {
|
||||
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Unauthorized");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package com.svlada.security.auth;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import com.svlada.security.model.JwtToken;
|
||||
import com.svlada.security.model.SafeJwtToken;
|
||||
import com.svlada.security.model.UnsafeJwtToken;
|
||||
import com.svlada.security.model.UserContext;
|
||||
|
||||
/**
|
||||
* An {@link org.springframework.security.core.Authentication} implementation that is designed for simple presentation
|
||||
* of JwtToken.
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* May 23, 2016
|
||||
*/
|
||||
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = 2877954820905567501L;
|
||||
|
||||
private JwtToken safeToken;
|
||||
private UnsafeJwtToken unsafeToken;
|
||||
|
||||
private UserContext userContext;
|
||||
|
||||
public JwtAuthenticationToken(UnsafeJwtToken unsafeToken) {
|
||||
super(null);
|
||||
this.unsafeToken = unsafeToken;
|
||||
this.
|
||||
setAuthenticated(false);
|
||||
}
|
||||
|
||||
public JwtAuthenticationToken(UserContext userContext, SafeJwtToken token, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.safeToken = token;
|
||||
this.userContext = userContext;
|
||||
super.setAuthenticated(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticated(boolean authenticated) {
|
||||
if (authenticated) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
|
||||
}
|
||||
super.setAuthenticated(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.userContext;
|
||||
}
|
||||
|
||||
public JwtToken getSafeToken() {
|
||||
return this.safeToken;
|
||||
}
|
||||
|
||||
public UnsafeJwtToken getUnsafeToken() {
|
||||
return unsafeToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eraseCredentials() {
|
||||
super.eraseCredentials();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package com.svlada.security.auth.ajax;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.svlada.security.auth.JwtAuthenticationToken;
|
||||
import com.svlada.security.model.JwtTokenFactory;
|
||||
import com.svlada.security.model.SafeJwtToken;
|
||||
import com.svlada.security.model.UserContext;
|
||||
import com.svlada.security.service.UserService;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 3, 2016
|
||||
*/
|
||||
@Component
|
||||
public class AjaxAuthenticationProvider implements AuthenticationProvider {
|
||||
private final JwtTokenFactory tokenFactory;
|
||||
private final UserService userService;
|
||||
|
||||
@Autowired
|
||||
public AjaxAuthenticationProvider(final JwtTokenFactory tokenFactory, final UserService userService) {
|
||||
this.tokenFactory = tokenFactory;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
Assert.notNull(authentication, "No authentication data provided.");
|
||||
|
||||
String username = (String) authentication.getPrincipal();
|
||||
String password = (String) authentication.getCredentials();
|
||||
|
||||
UserContext userContext = userService.loadUser(username, password);
|
||||
|
||||
SafeJwtToken safeJwtToken = tokenFactory.createSafeToken(userContext, userContext.getAuthorities());
|
||||
|
||||
return new JwtAuthenticationToken(userContext, safeJwtToken, userContext.getAuthorities());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package com.svlada.security.auth.ajax;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.svlada.common.ErrorCode;
|
||||
import com.svlada.common.ErrorResponse;
|
||||
import com.svlada.security.exceptions.AuthMethodNotSupportedException;
|
||||
import com.svlada.security.exceptions.JwtExpiredTokenException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 3, 2016
|
||||
*/
|
||||
@Component
|
||||
public class AjaxAwareAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
@Autowired
|
||||
public AjaxAwareAuthenticationFailureHandler(ObjectMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException e) throws IOException, ServletException {
|
||||
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
if (e instanceof BadCredentialsException) {
|
||||
mapper.writeValue(response.getWriter(), ErrorResponse.of("Invalid username or password", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
|
||||
} else if (e instanceof JwtExpiredTokenException) {
|
||||
mapper.writeValue(response.getWriter(), ErrorResponse.of("Token has expired", ErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED));
|
||||
} else if (e instanceof AuthMethodNotSupportedException) {
|
||||
mapper.writeValue(response.getWriter(), ErrorResponse.of(e.getMessage(), ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
|
||||
}
|
||||
|
||||
mapper.writeValue(response.getWriter(), ErrorResponse.of("Authentication failed", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package com.svlada.security.auth.ajax;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.WebAttributes;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.svlada.security.auth.JwtAuthenticationToken;
|
||||
import com.svlada.security.model.JwtToken;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 3, 2016
|
||||
*/
|
||||
@Component
|
||||
public class AjaxAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
@Autowired
|
||||
public AjaxAwareAuthenticationSuccessHandler(ObjectMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||
Authentication authentication) throws IOException, ServletException {
|
||||
JwtToken token = ((JwtAuthenticationToken) authentication).getSafeToken();
|
||||
|
||||
response.setStatus(HttpStatus.OK.value());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
mapper.writeValue(response.getWriter(), token);
|
||||
|
||||
clearAuthenticationAttributes(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes temporary authentication-related data which may have been stored
|
||||
* in the session during the authentication process..
|
||||
*
|
||||
*/
|
||||
protected final void clearAuthenticationAttributes(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
package com.svlada.security.auth.ajax;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.svlada.common.WebUtil;
|
||||
import com.svlada.security.exceptions.AuthMethodNotSupportedException;
|
||||
|
||||
/**
|
||||
* AjaxLoginProcessingFilter
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 3, 2016
|
||||
*/
|
||||
public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
|
||||
private static Logger logger = LoggerFactory.getLogger(AjaxLoginProcessingFilter.class);
|
||||
|
||||
private final AuthenticationSuccessHandler successHandler;
|
||||
private final AuthenticationFailureHandler failureHandler;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public AjaxLoginProcessingFilter(String defaultFilterProcessesUrl,
|
||||
AuthenticationSuccessHandler successHandler,
|
||||
AuthenticationFailureHandler failureHandler,
|
||||
ObjectMapper mapper) {
|
||||
super(defaultFilterProcessesUrl);
|
||||
this.successHandler = successHandler;
|
||||
this.failureHandler = failureHandler;
|
||||
this.objectMapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
|
||||
throws AuthenticationException, IOException, ServletException {
|
||||
if (!HttpMethod.POST.name().equals(request.getMethod()) || !WebUtil.isAjax(request)) {
|
||||
throw new AuthMethodNotSupportedException("Authentication method not supported");
|
||||
}
|
||||
|
||||
LoginRequest loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class);
|
||||
|
||||
if (StringUtils.isBlank(loginRequest.getUsername()) || StringUtils.isBlank(loginRequest.getPassword())) {
|
||||
throw new AuthenticationServiceException("Username or Password not provided");
|
||||
}
|
||||
|
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
|
||||
|
||||
return this.getAuthenticationManager().authenticate(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
|
||||
Authentication authResult) throws IOException, ServletException {
|
||||
successHandler.onAuthenticationSuccess(request, response, authResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException failed) throws IOException, ServletException {
|
||||
SecurityContextHolder.clearContext();
|
||||
failureHandler.onAuthenticationFailure(request, response, failed);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.svlada.security.auth.ajax;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Model intended to be used for AJAX based authentication.
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 3, 2016
|
||||
*/
|
||||
|
||||
public class LoginRequest {
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
@JsonCreator
|
||||
public LoginRequest(@JsonProperty("username") String username, @JsonProperty("password") String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
}
|
||||
46
src/main/java/com/svlada/security/config/JwtSettings.java
Normal file
46
src/main/java/com/svlada/security/config/JwtSettings.java
Normal file
@ -0,0 +1,46 @@
|
||||
package com.svlada.security.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "demo.security.jwt")
|
||||
public class JwtSettings {
|
||||
/**
|
||||
* {@link JwtToken} will expire after this time.
|
||||
*/
|
||||
private Integer tokenExpirationTime;
|
||||
|
||||
/**
|
||||
* Token issuer.
|
||||
*/
|
||||
private String tokenIssuer;
|
||||
|
||||
/**
|
||||
* Key is used to sign {@link JwtToken}.
|
||||
*/
|
||||
private String tokenSigningKey;
|
||||
|
||||
public Integer getTokenExpirationTime() {
|
||||
return tokenExpirationTime;
|
||||
}
|
||||
|
||||
public void setTokenExpirationTime(Integer tokenExpirationTime) {
|
||||
this.tokenExpirationTime = tokenExpirationTime;
|
||||
}
|
||||
|
||||
public String getTokenIssuer() {
|
||||
return tokenIssuer;
|
||||
}
|
||||
public void setTokenIssuer(String tokenIssuer) {
|
||||
this.tokenIssuer = tokenIssuer;
|
||||
}
|
||||
|
||||
public String getTokenSigningKey() {
|
||||
return tokenSigningKey;
|
||||
}
|
||||
|
||||
public void setTokenSigningKey(String tokenSigningKey) {
|
||||
this.tokenSigningKey = tokenSigningKey;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package com.svlada.security.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.svlada.security.RestAuthenticationEntryPoint;
|
||||
import com.svlada.security.auth.ajax.AjaxAuthenticationProvider;
|
||||
import com.svlada.security.auth.ajax.AjaxLoginProcessingFilter;
|
||||
|
||||
/**
|
||||
* WebSecurityConfig
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 3, 2016
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
|
||||
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
|
||||
|
||||
@Autowired private RestAuthenticationEntryPoint authenticationEntryPoint;
|
||||
@Autowired private AuthenticationSuccessHandler successHandler;
|
||||
@Autowired private AuthenticationFailureHandler failureHandler;
|
||||
@Autowired private AjaxAuthenticationProvider ajaxAuthenticationProvider;
|
||||
|
||||
@Autowired private AuthenticationManager authenticationManager;
|
||||
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
|
||||
@Bean
|
||||
protected AjaxLoginProcessingFilter buildAjaxLoginProcessingFilter() throws Exception {
|
||||
AjaxLoginProcessingFilter filter = new AjaxLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
|
||||
filter.setAuthenticationManager(this.authenticationManager);
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Override
|
||||
public AuthenticationManager authenticationManagerBean() throws Exception {
|
||||
return super.authenticationManagerBean();
|
||||
}
|
||||
|
||||
protected void configure(AuthenticationManagerBuilder auth) {
|
||||
auth.authenticationProvider(ajaxAuthenticationProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf().disable() // We don't need CSRF for JWT based authentication
|
||||
.exceptionHandling()
|
||||
.authenticationEntryPoint(this.authenticationEntryPoint)
|
||||
|
||||
.and()
|
||||
.sessionManagement()
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll() // Login end-point
|
||||
.and()
|
||||
.addFilterBefore(buildAjaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package com.svlada.security.exceptions;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 4, 2016
|
||||
*/
|
||||
public class AuthMethodNotSupportedException extends AuthenticationServiceException {
|
||||
private static final long serialVersionUID = 3705043083010304496L;
|
||||
|
||||
public AuthMethodNotSupportedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package com.svlada.security.exceptions;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
import com.svlada.security.model.JwtToken;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 3, 2016
|
||||
*/
|
||||
public class JwtExpiredTokenException extends AuthenticationException {
|
||||
private static final long serialVersionUID = -5959543783324224864L;
|
||||
|
||||
private JwtToken token;
|
||||
|
||||
public JwtExpiredTokenException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public JwtExpiredTokenException(JwtToken token, String msg, Throwable t) {
|
||||
super(msg, t);
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String token() {
|
||||
return this.token.getToken();
|
||||
}
|
||||
}
|
||||
5
src/main/java/com/svlada/security/model/JwtToken.java
Normal file
5
src/main/java/com/svlada/security/model/JwtToken.java
Normal file
@ -0,0 +1,5 @@
|
||||
package com.svlada.security.model;
|
||||
|
||||
public interface JwtToken {
|
||||
String getToken();
|
||||
}
|
||||
74
src/main/java/com/svlada/security/model/JwtTokenFactory.java
Normal file
74
src/main/java/com/svlada/security/model/JwtTokenFactory.java
Normal file
@ -0,0 +1,74 @@
|
||||
package com.svlada.security.model;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.svlada.security.config.JwtSettings;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.lang.Collections;
|
||||
|
||||
/**
|
||||
* Factory class that should be always used to create {@link JwtToken}.
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* May 31, 2016
|
||||
*/
|
||||
@Component
|
||||
public class JwtTokenFactory {
|
||||
@Autowired private JwtSettings settings;
|
||||
|
||||
/**
|
||||
* Factory method for issuing new JWT Tokens.
|
||||
*
|
||||
* @param username
|
||||
* @param roles
|
||||
* @return
|
||||
*/
|
||||
public SafeJwtToken createSafeToken(UserContext userContext, final Collection<GrantedAuthority> roles) {
|
||||
if (StringUtils.isBlank(userContext.getUsername())) {
|
||||
throw new IllegalArgumentException("Cannot create JWT Token without username");
|
||||
}
|
||||
|
||||
if (Collections.isEmpty(roles)) {
|
||||
throw new IllegalArgumentException("Cannot create JWT Token without roles");
|
||||
}
|
||||
|
||||
DateTime currentTime = new DateTime();
|
||||
|
||||
Claims claims = Jwts.claims();
|
||||
claims.put("roles", AuthorityUtils.authorityListToSet(roles));
|
||||
|
||||
String token = Jwts.builder()
|
||||
.setIssuer(settings.getTokenIssuer())
|
||||
.setSubject(userContext.getUsername())
|
||||
.setClaims(claims)
|
||||
.setIssuedAt(currentTime.toDate())
|
||||
.setExpiration(currentTime.plusMinutes(settings.getTokenExpirationTime()).toDate())
|
||||
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
|
||||
.compact();
|
||||
|
||||
return new SafeJwtToken(token, claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsafe version of JWT token is created.
|
||||
*
|
||||
* <strong>WARNING:</strong> Token signature validation is not performed.
|
||||
*
|
||||
* @param tokenPayload
|
||||
* @return unsafe version of JWT token.
|
||||
*/
|
||||
public UnsafeJwtToken createUnsafeToken(String tokenPayload) {
|
||||
return new UnsafeJwtToken(tokenPayload);
|
||||
}
|
||||
}
|
||||
30
src/main/java/com/svlada/security/model/SafeJwtToken.java
Normal file
30
src/main/java/com/svlada/security/model/SafeJwtToken.java
Normal file
@ -0,0 +1,30 @@
|
||||
package com.svlada.security.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
|
||||
/**
|
||||
* Raw representation of JWT Token.
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* May 31, 2016
|
||||
*/
|
||||
public final class SafeJwtToken implements JwtToken {
|
||||
private final String rawToken;
|
||||
@JsonIgnore private Claims claims;
|
||||
|
||||
protected SafeJwtToken(final String token, Claims claims) {
|
||||
this.rawToken = token;
|
||||
this.claims = claims;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return this.rawToken;
|
||||
}
|
||||
|
||||
public Claims getClaims() {
|
||||
return claims;
|
||||
}
|
||||
}
|
||||
35
src/main/java/com/svlada/security/model/UnsafeJwtToken.java
Normal file
35
src/main/java/com/svlada/security/model/UnsafeJwtToken.java
Normal file
@ -0,0 +1,35 @@
|
||||
package com.svlada.security.model;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
|
||||
public class UnsafeJwtToken implements JwtToken {
|
||||
private String token;
|
||||
|
||||
public UnsafeJwtToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates JWT Token signature.
|
||||
*
|
||||
*/
|
||||
public void validateToken(String signingKey) {
|
||||
Jwts.parser().setSigningKey(signingKey).parseClaimsJws(this.token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Claims object from the rawToken.
|
||||
*
|
||||
* @param signingKey
|
||||
* @return
|
||||
*/
|
||||
public Claims parseClaims(String signingKey) {
|
||||
return Jwts.parser().setSigningKey(signingKey).parseClaimsJws(token).getBody();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
35
src/main/java/com/svlada/security/model/UserContext.java
Normal file
35
src/main/java/com/svlada/security/model/UserContext.java
Normal file
@ -0,0 +1,35 @@
|
||||
package com.svlada.security.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 4, 2016
|
||||
*/
|
||||
public class UserContext {
|
||||
private final String username;
|
||||
private final String email;
|
||||
private final List<GrantedAuthority> authorities;
|
||||
|
||||
public UserContext(String username, String email, List<GrantedAuthority> authorities) {
|
||||
this.username = username;
|
||||
this.email = email;
|
||||
this.authorities = authorities;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public List<GrantedAuthority> getAuthorities() {
|
||||
return authorities;
|
||||
}
|
||||
}
|
||||
16
src/main/java/com/svlada/security/model/UserRole.java
Normal file
16
src/main/java/com/svlada/security/model/UserRole.java
Normal file
@ -0,0 +1,16 @@
|
||||
package com.svlada.security.model;
|
||||
|
||||
/**
|
||||
* Enumeration of user Roles.
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 3, 2016
|
||||
*/
|
||||
public enum UserRole {
|
||||
ADMIN, INSTRUCTOR, PARTICIPANT, SUPERADMIN;
|
||||
|
||||
public String authority() {
|
||||
return "ROLE_" + this.name();
|
||||
}
|
||||
}
|
||||
27
src/main/java/com/svlada/security/service/UserService.java
Normal file
27
src/main/java/com/svlada/security/service/UserService.java
Normal file
@ -0,0 +1,27 @@
|
||||
package com.svlada.security.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.svlada.security.model.UserContext;
|
||||
import com.svlada.security.model.UserRole;
|
||||
|
||||
/**
|
||||
* Mock implementation.
|
||||
*
|
||||
* @author vladimir.stankovic
|
||||
*
|
||||
* Aug 4, 2016
|
||||
*/
|
||||
@Service
|
||||
public class UserService {
|
||||
public UserContext loadUser(String username, String password) {
|
||||
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
|
||||
authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.authority()));
|
||||
return new UserContext(username, "svlada@gmail.com", authorities);
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
server.port=9966
|
||||
6
src/main/resources/application.yml
Normal file
6
src/main/resources/application.yml
Normal file
@ -0,0 +1,6 @@
|
||||
server.port: 9966
|
||||
spring.profiles: default
|
||||
demo.security.jwt:
|
||||
tokenExpirationTime: 2 # Number of minutes
|
||||
tokenIssuer: http://svlada.com
|
||||
tokenSigningKey: xm8EV6Hy5RMFK4EEACIDAwQus
|
||||
7
src/main/resources/logback.xml
Normal file
7
src/main/resources/logback.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<include resource="org/springframework/boot/logging/logback/base.xml"/>
|
||||
<logger name="org.springframework.web" level="INFO"/>
|
||||
<logger name="org.springframework" level="ERROR"/>
|
||||
<logger name="com.svlada" level="ALL"/>
|
||||
</configuration>
|
||||
Loading…
Reference in New Issue
Block a user