I'm trying to create REST API and web/MVC application in Spring. They both should use the same service layer. Can I somehow use two completely different configurations in Spring (Token authentication for API, cookies for web, 404 page for web, etc)? Or should I make two independent Spring applications?
Spring-MVC
and Spring-Security
Spring-MVC configuration by default facilitates
Controller can return ModelAndView for Web application view serving purpose.
Controller can be used as RestController
where response is by default processed by HttpMessageConverters
where controller methods used as Rest-API
However we can use Spring-Security
which is a filter based framework and it acts as a
security-wall(http-firewall) between your Rest-APIs and client-app consuming Rest API
Or
security-wall(http-firewall) between Spring-MVC
application and end-user
- Secure web application
- Login form for authenticating first time.
- Session for subsequent requests authentication.
- Hence Every requests will have state i.e, stateful requests
- Secure Rest API(Token based authentication)
- Every requests will be stateless
- Token based authentication should be preferred
- Session will not work in case if request is from cross-origin(different origin)
Implementation-type 1. Rest APIs should only accessed if auth token is present and valid.
Implementation-type 2. Rest APIs can be accessed by auth token as well as session.
"/api/**"
rest of url's will not be considered by this configuration. This http configuration will be configured for stateless. And you should configure an implementation of OncePerRequestFilter
(Say JwtAuthFilter
) and filter order can be before UsernamePasswordAuthenticationFilter
or BasicAuthenticationFilter
. But your filter should read the header for auth token, validate it and should create Authentication
object and set it to SecurityContext
without fail.JwtAuthFilter
but configures UsernamePasswordAuthenticationFilter
(.formLogin()
does this for you)@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = "com.gmail.nlpraveennl")
public class SpringSecurityConfig
{
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
@Configuration
@Order(1)
public static class RestApiSecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
private JwtAuthenticationTokenFilter jwtauthFilter;
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.antMatcher("/api/**")
.authorizeRequests()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/**").hasAnyRole("APIUSER")
.and()
.addFilterBefore(jwtauthFilter, UsernamePasswordAuthenticationFilter.class);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
@Configuration
@Order(2)
public static class LoginFormSecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
public void configureInMemoryAuthentication(AuthenticationManagerBuilder auth) throws Exception
{
auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("admin@123#")).roles("ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.antMatcher("/**").authorizeRequests()
.antMatchers("/resources/**").permitAll()
.antMatchers("/**").hasRole("ADMIN")
.and().formLogin();
http.sessionManagement().maximumSessions(1).expiredUrl("/login?expired=true");
}
}
}
"/**"
UsernamePasswordAuthenticationFilter
and JwtAuthFilter
but JwtAuthFilter
should be configured before UsernamePasswordAuthenticationFilter
.UsernamePasswordAuthenticationFilter
and attemptAuthentication method of UsernamePasswordAuthenticationFilter
will get invoked if there is no valid auth object in SecurityContext
. If JwtAuthFilter
validates token and sets auth object to SecurityContext
then even if filter chain reaches UsernamePasswordAuthenticationFilter
attemptAuthentication method will not be invoked as there is already an authentication object set in SecurityContext
.@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = "com.gmail.nlpraveennl")
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
private JwtAuthenticationTokenFilter jwtauthFilter;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
public void configureInMemoryAuthentication(AuthenticationManagerBuilder auth) throws Exception
{
auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("admin@123#")).roles("ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.antMatcher("/**").authorizeRequests()
.antMatchers("/resources/**").permitAll()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/**").hasAnyRole("APIUSER","ADMIN")
.antMatchers("/**").hasRole("ADMIN")
.and()
.formLogin()
.and()
.addFilterBefore(jwtauthFilter, UsernamePasswordAuthenticationFilter.class);
http.sessionManagement().maximumSessions(1).expiredUrl("/login?expired=true");
}
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
}
This is all about both type of implementation, you can go for any type of implementation depending upon your requirement. And for both implementation type JwtAuthenticationTokenFilter
and JwtTokenUtil
is common and is given below.
JwtAuthenticationTokenFilter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException
{
final String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer "))
{
String authToken = header.substring(7);
try
{
String username = jwtTokenUtil.getUsernameFromToken(authToken);
if (username != null)
{
if (jwtTokenUtil.validateToken(authToken, username))
{
// here username should be validated with database and get authorities from database if valid
// Say just to hard code
List<GrantedAuthority> authList = new ArrayList<>();
authList.add(new SimpleGrantedAuthority("ROLE_APIUSER"));
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, authList);
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
else
{
System.out.println("Token has been expired");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
}
catch (Exception e)
{
System.out.println("Unable to get JWT Token, possibly expired");
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
}
chain.doFilter(request, response);
}
}
JwtTokenUtil
@Component
public class JwtTokenUtil implements Serializable
{
private static final long serialVersionUID = 8544329907338151549L;
// public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60 * 1000; // 5 Hours
public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 1000; // 5 Minutes
private String secret = "my-secret";
public String getUsernameFromToken(String token)
{
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token)
{
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver)
{
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token)
{
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token)
{
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(String username)
{
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, username);
}
private String doGenerateToken(Map<String, Object> claims, String subject)
{
return "Bearer "+Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY)).signWith(SignatureAlgorithm.HS512, secret).compact();
}
public Boolean validateToken(String token, String usernameFromToken)
{
final String username = getUsernameFromToken(token);
return (username.equals(usernameFromToken) && !isTokenExpired(token));
}
}
You can download working example from my github repository link given below.
Implementation type-1
Implementation type-2
If you are curious about sequence of execution in Spring Security you can refer my answer here -> How spring security filter chain works
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With