Paul Woods
April 6th, 2016
mr.paul.woods@gmail.com
This talk shows how to take an application (Spring Boot + Gradle + Thymeleaf + Spring Data JPA) and add spring security to it.
We have a HR application called EmployeeDB. It allows us to maintain our employees, thier profiles (addresses) and thier salaries. It currently has no authentication or authorization.
You can clone the source code and follow along.
https://github.com/paulwoods/employeedb.git
Run the project in the 1-initial folder
Our first step is to add the Spring Security dependencies to our project.
build.gradle
...
dependencies {
...
compile('org.springframework.boot:spring-boot-starter-security')
...
}
...
console output
2016-03-14 19:34:09.256 INFO 11952 --- [ost-startStop-1] b.a.s.AuthenticationManagerConfiguration :
Using default security password: 16a01371-b881-4a40-b9f7-288e3e4ee7b2
2016-03-14 19:34:09.469 INFO 11952 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: OrRequestMatcher [requestMatchers=[Ant [pattern='/css/**'], Ant [pattern='/js/**'], Ant [pattern='/images/**'], Ant [pattern='/**/favicon.ico'], Ant [pattern='/error']]], []
Having a single user name for all users is a bad practice. And looking through the logs for the password is time consuming and annoying. We can solve this by change the user name, and setting the password to a known value.
src\main\resources\application.properties
...
security.user.name=paul
security.user.password=123456
...
Now, when you run the application, and it asks for the user and password, enter paul and 123456 to login.
Of course, there is only one account. You probably need to store multiple users.
We are goinging to defines the domains / tables to store our users, and map the roles they are granted.
src\main\groovy\org\mrpaulwoods\employeedb\security\Role.groovy
package org.mrpaulwoods.employee.security
enum Role {
ROLE_USER,
ROLE_ADMIN
}
src\main\groovy\org\mrpaulwoods\employeedb\security\User.groovypackage org.mrpaulwoods.employee.security
import org.springframework.security.core.userdetails.UserDetails
import javax.persistence.*
@Entity
class User implements UserDetails {
@Id @GeneratedValue Long id
@Column(unique=true) String username
String password
boolean enabled = true
boolean accountNonExpired = true
boolean accountNonLocked = true
boolean credentialsNonExpired = true
@OneToMany(mappedBy = "user", fetch=FetchType.EAGER, cascade = CascadeType.ALL)
List<Authority> authorities = []
}
src\main\groovy\org\mrpaulwoods\employeedb\security\Authority.groovypackage org.mrpaulwoods.employee.security
import org.springframework.security.core.GrantedAuthority
import javax.persistence.*
@Entity
class Authority implements GrantedAuthority {
@Id @GeneratedValue
Long id
@ManyToOne @JoinColumn(name="user_Id", nullable=false)
User user
String authority
}
src\main\groovy\org\mrpaulwoods\employeedb\security\EmployeeUserDetailsService.groovypackage org.mrpaulwoods.employee.security
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service
import javax.annotation.PostConstruct
@Service
class EmployeeUserDetailsService implements UserDetailsService {
@Autowired UserRepository userRepository
@Autowired AuthorityRepository authorityRepository
@Override UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
if(user)
user
else
throw new UsernameNotFoundException(username)
}
@PostConstruct void init() {
User user = userRepository.save(new User(username:"paulwoods", password:"alpha"))
user.authorities << authorityRepository.save(new Authority(user:user, authority: "ROLE_USER"))
user.authorities << authorityRepository.save(new Authority(user:user, authority: "ROLE_ADMIN"))
}
}
src\main\groovy\org\mrpaulwoods\employeedb\security\SecurityConfig.groovypackage org.mrpaulwoods.employee.security
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Configuration
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
@Configuration
@EnableWebSecurity
class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
EmployeeUserDetailsService employeeUserDetailsService
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.userDetailsService(employeeUserDetailsService)
}
@Override
protected void configure(HttpSecurity http) {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/css/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/images/**").permitAll()
.antMatchers("/login/**").permitAll()
.anyRequest().authenticated()
.and()
// configure the login form
.formLogin()
.loginPage("/login")
.permitAll()
.and()
// configure the logout form
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("http://www.google.com") // TODO: change url to "/" for production
.permitAll()
.and()
}
}
src\main\groovy\org\mrpaulwoods\employeedb\security\AuthorityRepository.groovypackage org.mrpaulwoods.employee.security
import org.springframework.data.jpa.repository.JpaRepository
interface AuthorityRepository extends JpaRepository<Authority, Long> {
}
src\main\groovy\org\mrpaulwoods\employeedb\security\UserRepository.groovypackage org.mrpaulwoods.employee.security
import org.springframework.data.jpa.repository.JpaRepository
interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username)
}
src\main\groovy\org\mrpaulwoods\employeedb\EmployeeApplication.groovypackage org.mrpaulwoods.employee
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
@SpringBootApplication
class EmployeeApplication extends WebMvcConfigurerAdapter {
static void main(String[] args) {
SpringApplication.run EmployeeApplication, args
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login")
registry.addViewController("/logout").setViewName("logout")
}
}
src\main\resources\templates\login.groovy<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns="http://www.w3.org/1999/xhtml"
layout:decorator="employee">
<body>
<div layout:fragment="body">
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div class="form-group">
<label>User Name:</label>
<input class="form-control" type="text" name="username"/>
</div>
<div class="form-group">
<label>Password:</label>
<input class="form-control" type="password" name="password"/>
</div>
<div><input type="submit" value="Sign In"/></div>
</form>
</div>
</body>
</html>
If you were to look at the user database table, then you would see that the passwords are stored in plain text, and need to be stored encrypted instead. We will add the BCrypt encryption library (already included by spring), as well as update the authentication manager to use the new encrypted passwords.
...
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
...
@PostConstruct
void init() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder()
User user = userRepository.save(new User(username: "paulwoods",
password: encoder.encode("beta")))
user.authorities << authorityRepository.save(
new Authority(user: user, authority: "ROLE_USER"))
user.authorities << authorityRepository.save(
new Authority(user: user, authority: "ROLE_ADMIN"))
}
...
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth
.userDetailsService(employeeUserDetailsService)
.passwordEncoder(new BCryptPasswordEncoder())
}
src\main\groovy\org\mrpaulwoods\employee\home\HomeController.groovy
@RequestMapping(method=GET)
String index(Principal principal) {
println "principal = $principal?.name"
"home/index"
}
This example accesses the principal from a Service.
src\main\groovy\org\mrpaulwoods\employee\home\HomeService.groovy
...
import org.springframework.security.core.context.SecurityContextHolder
@Slf4j @Service @Transactional class HomeService {
void example() {
def principal = SecurityContextHolder.context?.authentication
log.info "user = $principal?.name"
}
}
This example accesses the principal from a Thymeleaf template.
src\main\resources\templates\employee.html
<ul class="nav navbar-nav navbar-right">
<li>
<a href="#" th:text="'Welcome, ' + ${#authentication.name}">Name</a>
</li>
</ul>
In some situations, you have a User object (that extends UserDetail), and you want that use to become the currently logged in user. Add these lines to set the logged-in user.
import org.springframework.security.core.Authentication
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
...
UserDetail user = ...
Authentication authentication = new UsernamePasswordAuthenticationToken(
user, user.password, user.authorities)
SecurityContextHolder.context.authentication = authentication
Authorization determines what the user can an cannot access. We set this up by giving each user a 1-or-more roles that they can do, then we configure the URLs and/or controllers to allow access based on the roles.
We've specified the URL based authorization in the SecurityConfig
src\main\groovy\org\mrpaulwoods\employeedb\security\SecurityConfig.groovy
.antMatchers("/webjars/**").permitAll()
.antMatchers("/css/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/images/**").permitAll()
.antMatchers("/login/**").permitAll()
.anyRequest().authenticated()
We can also specify class and method level authorizations
@Secured("ROLE_TELLER")
@PreAuthorize("hasAuthority('ROLE_TELLER')")
Remember, to add @EnableGlobalMethodSecurity(securedEnabled = true) to your SecurityConfig class
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)src\main\groovy\org\mrpaulwoods\employee\payroll\PayrollController.groovy...
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
...
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends WebSecurityConfigurerAdapter {
...
src\main\groovy\org\mrpaulwoods\employee\payroll\PayrollController.groovy...
import org.springframework.security.access.prepost.PreAuthorize
...
@Controller
@RequestMapping(value = "/payroll")
@Slf4j
@PreAuthorize('hasRole("ROLE_HR")')
class PayrollController {
...
src\main\groovy\org\mrpaulwoods\employee\security\SecurityConfig.groovy...
protected void configure(HttpSecurity http) {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/css/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/images/**").permitAll()
.antMatchers("/login/**").permitAll()
.antMatchers("/api/**").permitAll()
.anyRequest().authenticated()
.and()
...
src\main\groovy\org\mrpaulwoods\employee\api\ApiController.groovypackage org.mrpaulwoods.employee.api
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping(value="/api/1")
class ApiController {
@RequestMapping(value="/home")
Message home() {
new Message()
}
}
src\main\groovy\org\mrpaulwoods\employee\api\Message.groovypackage org.mrpaulwoods.employee.api
class Message {
String message = "hello, world"
}
src\main\groovy\org\mrpaulwoods\employeedb\security\SecurityConfig.groovy
...
import org.mrpaulwoods.employee.security.jwt.StatelessAuthenticationFilter
import org.mrpaulwoods.employee.security.jwt.StatelessLoginFilter
import org.mrpaulwoods.employee.security.jwt.TokenAuthenticationService
...
@Autowired
private TokenAuthenticationService tokenAuthenticationService
...
protected void configure(HttpSecurity http) {
// configure api security
.addFilterBefore(
new StatelessLoginFilter("/api/login", tokenAuthenticationService,
employeeUserDetailsService, authenticationManager()),
UsernamePasswordAuthenticationFilter)
.addFilterBefore(new StatelessAuthenticationFilter(tokenAuthenticationService),
UsernamePasswordAuthenticationFilter)
}
...
src\main\resources\application.properties
token.secret=the-secret-phrase-goes-here
src\main\groovy\org\mrpaulwoods\employee\security\jwt\StatelessAuthenticationFilter.groovy
package org.mrpaulwoods.employee.security.jwt
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.GenericFilterBean
import javax.servlet.FilterChain
import javax.servlet.ServletException
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
class StatelessAuthenticationFilter extends GenericFilterBean {
private final TokenAuthenticationService tokenAuthenticationService
public StatelessAuthenticationFilter(TokenAuthenticationService taService) {
this.tokenAuthenticationService = taService
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
SecurityContextHolder.context.authentication = tokenAuthenticationService.getAuthentication(req)
chain.doFilter req, res
}
}
src\main\groovy\org\mrpaulwoods\employee\security\jwt\StatelessLoginFilter.groovy
package org.mrpaulwoods.employee.security.jwt
import com.fasterxml.jackson.databind.ObjectMapper
import groovy.util.logging.Slf4j
import org.mrpaulwoods.employee.security.User
import org.springframework.security.authentication.AuthenticationManager
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.core.userdetails.UserDetailsService
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
import javax.servlet.FilterChain
import javax.servlet.ServletException
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Slf4j
class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter {
private final TokenAuthenticationService tokenAuthenticationService
private final UserDetailsService userDetailsService
protected StatelessLoginFilter(String urlMapping,
TokenAuthenticationService tokenAuthenticationService,
UserDetailsService userDetailsService,
AuthenticationManager authManager) {
super(new AntPathRequestMatcher(urlMapping))
this.userDetailsService = userDetailsService
this.tokenAuthenticationService = tokenAuthenticationService
setAuthenticationManager(authManager)
}
/** Parses the {username: "xxx", password:"yyy"} json string, and attempts to login **/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
final User user = new ObjectMapper().readValue(request.getInputStream(), User.class)
final UsernamePasswordAuthenticationToken loginToken = new UsernamePasswordAuthenticationToken(
user.username, user.password)
return getAuthenticationManager().authenticate(loginToken)
}
/** the login was successful. create the token **/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authentication) throws IOException, ServletException {
// Lookup the complete User object from the database and create an Authentication for it
final User authenticatedUser = userDetailsService.loadUserByUsername(authentication.getName())
final UserAuthentication userAuthentication = new UserAuthentication(authenticatedUser)
// Add the custom token as HTTP header to the response
tokenAuthenticationService.addAuthentication(response, userAuthentication)
// Add the authentication to the Security context
SecurityContextHolder.getContext().setAuthentication(userAuthentication)
}
}
src\main\groovy\org\mrpaulwoods\employee\security\jwt\TokenAuthenticationService.groovy
package org.mrpaulwoods.employee.security.jwt
import groovy.util.logging.Slf4j
import org.mrpaulwoods.employee.security.EmployeeUserDetailsService
import org.mrpaulwoods.employee.security.User
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.core.Authentication
import org.springframework.stereotype.Service
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Service
@Slf4j
public class TokenAuthenticationService {
private static final String AUTH_HEADER_NAME = "X-AUTH-TOKEN"
@Autowired
UserTokenService userTokenService
/** adds the current user's token to the header */
public void addAuthentication(HttpServletResponse response, UserAuthentication authentication) {
final User user = authentication.getDetails()
response.addHeader(AUTH_HEADER_NAME, userTokenService.userToToken(user))
}
/** retrieves the current user from the token and stores in authentication object */
public Authentication getAuthentication(HttpServletRequest request) {
final String token = request.getHeader(AUTH_HEADER_NAME)
if (!token) {
return null
}
final User user = userTokenService.tokenToUser(token)
if (!user) {
return null
}
return new UserAuthentication(user)
}
}
src\main\groovy\org\mrpaulwoods\employee\security\jwt\UserAuthentication.groovy
package org.mrpaulwoods.employee.security.jwt
import org.mrpaulwoods.employee.security.User
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
public class UserAuthentication implements Authentication {
private final User user
private boolean authenticated = true
public UserAuthentication(User user) {
this.user = user
}
@Override
public String getName() {
return user.getUsername()
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return user.getAuthorities()
}
@Override
public Object getCredentials() {
return user.getPassword()
}
@Override
public User getDetails() {
return user
}
@Override
public Object getPrincipal() {
return user.getUsername()
}
@Override
public boolean isAuthenticated() {
return authenticated
}
@Override
public void setAuthenticated(boolean authenticated) {
this.authenticated = authenticated
}
}
src\main\groovy\org\mrpaulwoods\employee\security\jwt\UserTokenService.groovy
package org.mrpaulwoods.employee.security.jwt
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import org.mrpaulwoods.employee.security.EmployeeUserDetailsService
import org.mrpaulwoods.employee.security.User
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
/**
* This class converts between Users and JWT Tokens.
*/
@Service
public class UserTokenService {
@Autowired
EmployeeUserDetailsService employeeUserDetailsService
@Value(value = '${token.secret}')
String secret
User tokenToUser(String token) {
assert secret
String username = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getSubject()
employeeUserDetailsService.loadUserByUsername username
}
String userToToken(User user) {
assert secret
Jwts.builder()
.setSubject(user.username)
.signWith(SignatureAlgorithm.HS512, secret)
.compact()
}
}
See the Spring Guide: