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
If requirement is
- 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)
then Implementation considerations
Implementation-type 1. Rest APIs should only accessed if auth token is present and valid.
- Limitation of this implementation type is, if web application wants to make AJAX calls to Rest API even though browser has valid session it won't allow to access Web-APIs.
- Here Rest API is only for stateless access.
Implementation-type 2. Rest APIs can be accessed by auth token as well as session.
- Here Rest API's can be accessed by any third party applications(cross-origin) by auth token.
- Here Rest API's can be accessed in web application(same-origin) through AJAX calls.
Implementation-type 1
- It has multiple http security configuration(two http security configuration)
- where http configuration of @order(1) will authorize only
"/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.
- And http configuration of @order(2) will authorize if request is not qualified for first order http configuration. And this configuration does not configures
JwtAuthFilter
but configures UsernamePasswordAuthenticationFilter
(.formLogin()
does this for you)
And the configuration code for this implementation is given below
@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");
}
}
}
Implementation-type 2
- It has only one http security configuration
- where http configuration will authorize all
"/**"
- Here this http configuration is configured for both
UsernamePasswordAuthenticationFilter
and JwtAuthFilter
but JwtAuthFilter
should be configured before UsernamePasswordAuthenticationFilter
.
- Trick used here is if there is no Authorization header filter chain just continues to
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
.
And the configuration code for this implementation is given below
@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<
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…