Spring Boot Security 5 Basic
Powerful and highly customizable authentication and access-control.
Initializer
1. Dependencies
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
}
1. Default Login/Logout page
1.1 Spring Security generated default password
Run your project, in the log, you’ll find your password
2046-99-21 39:29:44.043 INFO 1733 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: 1d4d7018-a42a-4418-afb1-7565250facdd
2046-99-21 39:29:44.112 INFO 1733 --- [ restartedMain] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: blah
1.2 Test with default login
- Default login form
http://localhost:8080/
, directs tohttp://localhost:8080/login
, default login formhttp://localhost:8080/home
, directs tohttp://localhost:8080/login
, default login formhttp://localhost:8080/api
, directs tohttp://localhost:8080/login
, default login form
- login details
- The username is
user
, and the password is1d4d7018-a42a-4418-afb1-7565250facdd
- Test using
curl
andbasic auth
\$ curl -u user:1d4d7018-a42a-4418-afb1-7565250facdd localhost:8080/api security api \$ echo -n "user:1d4d7018-a42a-4418-afb1-7565250facdd" | base64 dXNlcjoxZDRkNzAxOC1hNDJhLTQ0MTgtYWZiMS03NTY1MjUwZmFjZGQ= \$ curl -H 'Accept:application/json' -H 'Authorization:Basic dXNlcjoxZDRkNzAxOC1hNDJhLTQ0MTgtYWZiMS03NTY1MjUwZmFjZGQ=' localhost:8080/api security api
- The username is
- Default logout function
http://localhost:8080/logout
1.3 config username/pwd in application.properties
file
spring.security.user.name=whatIsYourUserName
spring.security.user.password=hardToGuess
- Restart project the Spring generated security password is gone
- username and password is case-senstive
2. Customised login/logout page
2.1 Set it up
-
new
LoginController
@Controller public class LoginController { @RequestMapping("/loginPage") public String login() { return "login_page"; } @RequestMapping("/logoutPage") public String logout() { return "logout_page"; } }
-
new Views
resources/templates/login_page.html
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Custom Login</title>
</head>
<body>
<div>
<form th:action="@{/loginPage}" method="post">
<fieldset>
<legend>Custom Login</legend>
<div th:if="${param.error}">
<p style="color:red">invalid username or password.</p>
</div>
<div th:if="${param.logout}">
<p style="color: green">You have been logged out.</p>
</div>
<label for="username">Username</label>
<input type="text" id="username" name="username" />
<label for="password">Password</label>
<input type="text" id="password" name="password" />
<div class="form-actions">
<button class="btn" type="submit">Log In</button>
</div>
</fieldset>
</form>
</div>
</body>
</html>
resources/templates/logout_page.html
<html xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Custom Logout</title>
</head>
<body>
<h2>Custom Logout Page</h2>
<form th:action="@{/logoutPage}" method="post">
<input type="submit" value="Log out" />
</form>
</body>
</html>
2.2 Customize security config
SecurityConfig.java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/*
Copy from WebSecurityConfigurerAdapter, now we need to override it
*/
// protected void configure(HttpSecurity http) throws Exception {
// logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
// http.authorizeRequests()
// .anyRequest().authenticated()
// .and()
// .formLogin()
// .and()
// .httpBasic();
// }
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/loginPage")
.permitAll()
.and()
.logout()
.logoutUrl("/logoutPage")
.logoutSuccessUrl("/loginPage?logout")
.permitAll();
}
}
2.3 Test customized login page
3. Multiple Login Page
3.1 Set up vew for multi login pages
Add following files:
resources/templates/admin/home.html
resources/templates/admin/login.html
resources/templates/user/home.html
resources/templates/user/login.html
Example code from resources/templates/admin/login.html
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Admin Login</title>
</head>
<body>
<h2>Welcome to admin login page</h2>
<form th:action="@{/admin/login}" method="post">
<div id="loginTable">
<table>
<tbody>
<tr>
<td><label for="username">Username</label></td>
<td><input type="text" id="username" name="username"></td>
</tr>
<tr>
<td><label for="password">Password</label></td>
<td><input type="text" name="password" id="password"></td>
</tr>
<tr>
<td><input type="submit" value="Sign In"></td>
</tr>
<tr>
<td colspan="2">
<div class="error" th:if="${param.error}">Invalid username or password.</div>
<div class="info" th:if="${param.logout}">You have been logged out.</div>
</td>
</tr>
</tbody>
</table>
</div>
</form>
</body>
</html>
3.2 Set up MvcConfig
Add new config file MvcConfig
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("user/home");
registry.addViewController("/admin/login").setViewName("admin/login");
registry.addViewController("/admin/home").setViewName("admin/home");
registry.addViewController("/user/login").setViewName("user/login");
registry.addViewController("/user/home").setViewName("user/home");
}
}
3.3 Set up SecurityConfig
- Add new config file
SecurityConfig
- Add
AdminSecurityConfig
- Add
UserSecurityConfig
- Configure InMemoryUserDetails for ADMIN and User roles
- PasswordEncoder
Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Configuration
@Order(1)
public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/admin/**")
.authorizeRequests()
.antMatchers("/css/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/admin/login")
.defaultSuccessUrl("/admin/home")
.permitAll()
.and()
.logout()
.logoutUrl("/admin/logout")
.permitAll();
}
}
@Configuration
@Order(2)
public static class UserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/user/**")
.authorizeRequests()
.antMatchers("/css/**").permitAll()
.antMatchers("/user/**").hasRole("USER")
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/user/home")
.permitAll()
.and()
.logout()
.logoutUrl("/user/logout")
.permitAll();
}
}
@Bean
public static PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user")
.password(encoder().encode("123456"))
.roles("user")
.build());
manager.createUser(User.withUsername("admin")
.password(encoder().encode("123456"))
.roles("ADMIN")
.build());
return manager;
}
3.4 Test Multi login form
It creates two roles and got two in memory users. You can go to http://localhost:8080/admin/login
to login admin users or http://localhost:8080/user/login
to login in as USER role users.
4. hasRole and hasAuthority
4.1 Set up controller
HomeController.java
@RestController
@AllArgsConstructor
@RequestMapping("/employee")
public class EmployeeController {
@GetMapping
public String findAll() { return "list"; }
@DeleteMapping("/{id}")
public Long delete(@PathVariable Long id) { return id; }
}
SecurityConfig.java
Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Configuration
@Order(1)
public static class UserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
}
@Bean
public static PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("admin")
.password(encoder().encode("123456"))
.roles("ADMIN")
.build());
manager.createUser(User.withUsername("user")
.password(encoder().encode("123456"))
.roles("USER")
.build());
manager.createUser(User.withUsername("guest")
.password(encoder().encode("123456"))
.roles("GUEST")
.build());
return manager;
}
}
4.2 Test users
curl -H 'Accept:application/json' -u guest:123456 localhost:8080/employee
list
curl -H 'Accept:application/json' -u admin:123456 localhost:8080/employee
list
curl -H 'Accept:application/json' -u fake:123456 localhost:8080/employee
(nothing)
4.3 hasRole in HttpSecurity Filter
@Configuration
@Order(1)
public static class UserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/employee**").hasRole("USER")
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
}
$ curl -H 'Accept:application/json' -u user:123456 localhost:8080/employee
list
$ curl -H 'Accept:application/json' -u guest:123456 localhost:8080/employee
{"timestamp":"2046-13-24T07:07:40.496+00:00","status":403,"error":"Forbidden","message":"Forbidden","path":"/employee"}
The guest user got 403
error, because it doesn’t have USER
role.
NB:
.antMatchers(HttpMethod.GET, "/employee**").hasRole("USER")
notice the/
in url
4.4 hasRole in method level
SecurityConfig.java
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {}
EmployeeController
@GetMapping
@PreAuthorize("hasRole('USER')")
public String findAll() { return "list"; }
$ curl -H 'Accept:application/json' -u user:123456 localhost:8080/employee
list
$ curl -H 'Accept:application/json' -u guest:123456 localhost:8080/employee
{"timestamp":"2046-13-24T07:18:16.635+00:00","status":403,"error":"Forbidden","trace":"..."}
NB:
- Mind the annotation on
SecurityConfig.java
@EnableGlobalMethodSecurity(prePostEnabled = true)
4.5 hasAuthrise in SecurityConfig
@Configuration
//@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Configuration
@Order(1)
public static class UserSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/employee**").hasAuthority("read")
.antMatchers(HttpMethod.DELETE, "/employee/**").hasAuthority("delete")
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("admin")
.password(encoder().encode("123456"))
.authorities("read","delete")
.build());
manager.createUser(User.withUsername("user")
.password(encoder().encode("123456"))
.authorities("read")
.build());
manager.createUser(User.withUsername("guest")
.password(encoder().encode("123456"))
.roles("GUEST")
.build());
return manager;
}
$ curl -X DELETE -u admin:123456 localhost:8080/employee/100
100%
$ curl -X DELETE -u user:123456 localhost:8080/employee/100
{"timestamp":"2046-13-24T07:42:14.023+00:00","status":403,"error":"Forbidden","message":"Forbidden","path":"/employee/100"}%
NB:
.antMatchers(HttpMethod.DELETE, "/employee/**").hasAuthority("delete")
mind the/employee/
, it has two/
s
4.6 hasAuthrise in method level
EmployeeController.java
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyAuthority('delete')")
public Long delete(@PathVariable Long id) { return id; }
$ curl -X DELETE -u admin:123456 localhost:8080/employee/100
100%
$ curl -X DELETE -u guest:123456 localhost:8080/employee/100
{"timestamp":"2046-13-24T07:46:33.765+00:00","status":403,"error":"Forbidden","message":"Forbidden","path":"/employee/100"}%
5. Whitelist
-
Set up Controller
@GetMapping("/home/ip") @ResponseBody public String ip(HttpServletRequest request) { return request.getRemoteAddr(); }
-
Whitelist in your security config
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/home/ip").hasIpAddress("192.168.1.0") // <-- whitelist ... }
-
Customize whitelist
public class CustomIpAuthenticationProvider implements AuthenticationProvider { private final List<String> ipWhiteList = new ArrayList<>(); public CustomIpAuthenticationProvider() { // this list can be populated from DB ipWhiteList.add("192.168.1.0"); ipWhiteList.add("192.168.1.1"); ipWhiteList.add("192.168.1.1"); } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails(); String ip = details.getRemoteAddress(); if (!ipWhiteList.contains(ip)) { throw new BadCredentialsException("Invalid Ip"); } return authentication; } @Override public boolean supports(Class<?> authentication) { return true; } }
6. Custom Authentication
6.1 Set up
application.properteis
spring.h2.console.enabled=true
spring.h2.console.path=/h2
spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false
spring.datasource.url=jdbc:h2:D://data//test;DB_CLOSE_DELAY=-1;
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
SecurityConfig.java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().ignoringAntMatchers("/h2/**")
.and()
.headers().frameOptions().sameOrigin()
.and()
.authorizeRequests()
.antMatchers("/h2/**").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
CustomUsernamePasswordAuthenticationProvider.java
@Component
public class CustomUsernamePasswordAuthenticationProvider implements AuthenticationProvider {
final private EmployeeRepo employeeRepo;
public CustomUsernamePasswordAuthenticationProvider(EmployeeRepo employeeRepo) {
this.employeeRepo = employeeRepo;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
Employee employee = employeeRepo.findByName(username)
.orElseThrow(()-> new BadCredentialsException("Invalid username"));
String usernameDB = employee.getName();
String passwordDB = employee.getPassword();
boolean isPasswordCorrect = BCrypt.checkpw(password, passwordDB);
if (username.equals(usernameDB) && isPasswordCorrect) {
return new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
} else {
throw new BadCredentialsException("Invalid username or password");
}
}
@Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}
6.2 Set up Repo
EmployeeRepo.java
public interface EmployeeRepo extends JpaRepository<Employee, Long> {
Optional<Employee> findByName(String name);
}
6.3 Set up H2 database
- Start the project
- got to h2 db interface
Driver Class:org.h2.Driver
JDBC URL: jdbc:h2:D://data//test;DB_CLOSE_DELAY=-1;
User Name: sa
Password: (empty)
// we didn’t config it
- Insert a demo user
- id:1
- name: “user”
- password, go to
https://www.browserling.com/tools/bcrypt
to generate one
6.4 test
7. Block Attacks from an IP
7.1 Set up components
add dependency
compile group: 'com.google.guava', name: 'guava', version: '21.0'
LoginAttemptService.java
@Service
public class LoginAttemptService {
private static final Integer MAX_ATTAMPT = 3;
private LoadingCache<String, Integer> attemptCache;
public LoginAttemptService() {
this.attemptCache = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.DAYS)
.build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String key) throws Exception {
return 0;
}
});
}
public void loginSucceed(String ip) {
attemptCache.invalidate(ip);
}
public void loginFailed(String ip) {
Integer attempts = 0;
try {
attempts = attemptCache.get(ip);
} catch (ExecutionException e) {
e.printStackTrace();
}
attempts ++;
attemptCache.put(ip, attempts);
}
public boolean isBlocked(String ip) {
Integer attempts = 0;
try {
attempts = attemptCache.get(ip);
} catch (ExecutionException e) {
e.printStackTrace();
}
return attempts > MAX_ATTAMPT;
}
}
7.2 Set up event listeners
AuthenticationLoginFailureEventListener.java
@Component
public class AuthenticationLoginFailureEventListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> {
private LoginAttemptService loginAttemptService;
public AuthenticationLoginFailureEventListener(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
}
@Override
public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) {
WebAuthenticationDetails details = (WebAuthenticationDetails) event.getAuthentication().getDetails();
loginAttemptService.loginFailed(details.getRemoteAddress());
}
}
AuthenticationLoginSuccessEventListener.java
@Component
public class AuthenticationLoginSuccessEventListener implements ApplicationListener<AuthenticationSuccessEvent> {
private LoginAttemptService loginAttemptService;
public AuthenticationLoginSuccessEventListener(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
}
@Override
public void onApplicationEvent(AuthenticationSuccessEvent event) {
WebAuthenticationDetails webAuthenticationDetails = (WebAuthenticationDetails) event.getAuthentication().getDetails();
loginAttemptService.loginSucceed(webAuthenticationDetails.getRemoteAddress()
);
}
}
7.3 Inject services into CustomUsernamePasswordAuthenticationProvider
CustomUsernamePasswordAuthenticationProvider.java
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
final WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();
if (loginAttemptService.isBlocked(details.getRemoteAddress())) {
throw new BadCredentialsException("Invalid ip");
}
String username = authentication.getName();
String password = authentication.getCredentials().toString(); // not safe
Employee employee = employeeRepo.findByName(username)
.orElseThrow(()-> new BadCredentialsException("Invalid username"));
String usernameDB = employee.getName();
String passwordDB = employee.getPassword();
boolean isPasswordCorrect = BCrypt.checkpw(password, passwordDB);
if (username.equals(usernameDB) && isPasswordCorrect) {
return new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
} else {
throw new BadCredentialsException("Invalid username or password");
}
}
7.4 test
- Try to login but provide wrong password for five times.
- Try to login again, will get invalid ip error message.
8. Acquire user’s information
8.1 Set up
application.properties
spring.security.user.name=username
spring.security.user.password=123456
8.2 Implement
UserInfoController.java
@RestController
@Slf4j
public class UserInfoController {
@GetMapping("/userInfo")
public String userInfo(Authentication authentication) {
log.info("name: {}", authentication.getName());
log.info("principle: {}", authentication.getPrincipal());
log.info("details: {}", authentication.getDetails());
log.info("credentials: {}", authentication.getCredentials());
log.info("authorities: {}", authentication.getAuthorities());
return authentication.getName();
}
}
It logs with:
c.m.springsecurity.UserInfoController : name: user
c.m.springsecurity.UserInfoController : principle: user
c.m.springsecurity.UserInfoController : details: org.springframework.security.web.authentication.WebAuthenticationDetails@380f4: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: D4239E2FC21B82D9BADB6D5F25A29B79
c.m.springsecurity.UserInfoController : credentials: null
c.m.springsecurity.UserInfoController : authorities: []
details have info like:
- ip,
- session
8.3 keep credentials in the Authenticate object
Credentials will be removed by default. If you want the credential to be kept within the Authentication object, in Securityconfig.java
update:
SecurityConfig.java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// something no need to mention
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.eraseCredentials(false);
auth.authenticationProvider(customUsernamePasswordAuthenticationProvider);
}
}
In the log you’ll see: credentials: 123456
8.4 Other ways you get user’s info
- Current way is to inject
Authentication
directely to the controller’s parameter - You can also get
Authentication
inside of method body - You can use
Principle
rather thanAuthentication
@GetMapping("/userInfo")
public String userInfo(Authentication authentication) {
return authentication.getName();
}
@GetMapping("/userInfo2")
public String userInfo2() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication.getName();
}
@GetMapping("/userInfo3")
public String userInfo3(Principal principal) {
return principal.getName();
}
9. httpBasic vs. loginform
Spring security can read username/password from HttpServletRequest
with built in mechanisms like:
- Form Login: through an html form
- Basic Authentication: Basic HTTP Authentication
- Digest Authentication: Dont use, it’s unsecure
You can use either of them, or both of them.
9.1 code
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.successForwardUrl("/home")
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic()
.and()
.formLogin()
.defaultSuccessUrl("/index");
}
9.2 httpBasic
- Enabled by default
- A dialog form poped out, user needs to fill in username and password.
9.3 formlogin()
- Enabled by default
- default login page
- default logout url
9.4 Cookie and Session in Spring Security
formLogin
should useifRequired
session policyhttpBasic
should useSTATELESS
session policy
10. JWT Basic
Normal token process:
- normal token is just a string and doesn’t have user’s information
- the server got the token, then load user’s infom then find the resource the user can visit, then find if the user is authenticated
JWT
- jwt.io
- Algorithm: HS256, HS512
- Front end sent encoded token string
- Server side decode it with:
-
header: algorithm & token type
{ "alg":"HS256", "typ": "JWT" }
-
payload: data
{ "sub":"123456", "name":"moss gu", "iat":"15115115115100" }
-
veifigy signature
-
10.1 set up project
- Find gradle dependency,see:
https://github.com/jwtk/jjwt#dependencies
build.gradle
compile 'io.jsonwebtoken:jjwt-api:0.11.1'
runtime 'io.jsonwebtoken:jjwt-impl:0.11.1',
// Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
//'org.bouncycastle:bcprov-jdk15on:1.60',
'io.jsonwebtoken:jjwt-jackson:0.11.1' // or 'io.jsonwebtoken:jjwt-gson:0.11.1' for gson
HelloJwt.java
@Log4j2
public class HelloJwt {
@Test
public void generate() {
//creates a spec-compliant secure-random key:
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
var token = Jwts.builder().setSubject("moss").signWith(key).compact();
log.info("token: {}", token);
}
}
13:49:06.951 [main] INFO com.mossj.springsecurity.HelloJwt - token: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtb3NzIn0.RyxtLrNNj6TgumuuBe12Z7eaJls-T9rsP5tjp9B8s5o
You’ve got wrong dependencies if you get the following error message
Caused by: io.jsonwebtoken.lang.UnknownClassException:
Unable to load class named [io.jsonwebtoken.impl.crypto.MacProvider] from the thread context, current, or system/application ClassLoaders.
All heuristics have been exhausted.
Class could not be found.
Have you remembered to include the jjwt-impl.jar in your runtime classpath?
10.2 verify token in jwt.io
here is the token eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtb3NzIn0.0kK2GS_qEX1gUyIbil1PcL1rEpRmjsJinYtuO3iqJpI
- header:
eyJhbGciOiJIUzI1NiJ9
- payload:
eyJzdWIiOiJtb3NzIn0
- signature:
0kK2GS_qEX1gUyIbil1PcL1rEpRmjsJinYtuO3iqJpI
go to jwt.io
, paste in your encoded token, you will get:
- Header:
{ "alg": "HS256" }
- Payload:
{ "sub": "moss" }
-
Verify signature
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), (your-256-bit-secret) )
10.3 my dynamic secret key
@Test
public void generate() {
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
final String encoded = Base64.getEncoder().encodeToString(key.getEncoded());
log.info("encoded Key: {}", encoded);
final SecretKey secretKey = Keys.hmacShaKeyFor(encoded.getBytes());
var token = Jwts.builder().setSubject("moss").signWith(secretKey).compact();
log.info("token: {}", token);
final String subject = Jwts.parserBuilder()
.setSigningKey(secretKey).build()
.parseClaimsJws(token)
.getBody()
.getSubject();
log.info("sub: {}",subject);
assertEquals(subject, "moss");
}
Run it two times, see the log
springsecurity.HelloJwt - encoded Key: pRNsm7m8JoGl0q9uY8F/YquCcCA4rFPzjZqtMdzrqPk=
springsecurity.HelloJwt - token: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtb3NzIn0.xuBbV7r3wsaXVR9ePmFpmPFcVPaiiMjfDnzji1IuwvM
springsecurity.HelloJwt - sub: moss
// 2nd time
springsecurity.HelloJwt - encoded Key: 0jjJrHnpDY0h6mLAsVkbgt2mgauaovjDTjgzd8ep+v0=
springsecurity.HelloJwt - token: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtb3NzIn0.34t0B8XBCn53eAuchvvRI_pyaaHkTIDaaQH0eywfEWg
springsecurity.HelloJwt - sub: moss
the encoded keys are dynamic and random.
11. Use JWT for Authentication and Authorization
Baisicly, they’re two filters:
JwtAuthenticateFilter extends UsernamePasswordAuthenticationFilter
JwtAuthorizationFilter extends BasicAuthenticationFilter
11.1 Register two filters in SecurityConfig
Although they’re not impleted yet, we can have a whole idea about how to register them.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/guest").permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic()
.and()
.addFilter(new JwtAuthenticateFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
11.2 Set up some constents
SecurityConstants.java
public class SecurityConstants {
public static final String AUTH_LOGIN_URL = "/api/token";
public static final String JWT_SECRET = "pRNsm7m8JoGl0q9uY8F/YquCcCA4rFPzjZqtMdzrqPk=";
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String TOKEN_TYPE = "JWT";
public static final String TOKEN_ISSUER = "secure-api";
public static final String TOKEN_AUDIENCE = "secure-app";
private SecurityConstants() {
throw new IllegalStateException("Cannot create instance of static util class");
}
}
11.3 Set up JwtAuthenticateFilter
- NB:
setFilterProcessesUrl("/api/token");
-
Security enforcement:
parseData()
method. We should not use// not safe request.getParameter("username"); request.getParameter("password");
JwtAuthenticateFilter.class
public class JwtAuthenticateFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public JwtAuthenticateFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
setFilterProcessesUrl("/api/token");
}
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
LoginData loginData = parseData(request);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginData.getUsername(), loginData.getPassword());
return this.authenticationManager.authenticate(token);
}
private LoginData parseData(HttpServletRequest request) throws IOException {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(request.getInputStream(), LoginData.class);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
final User user = (User) authResult.getPrincipal();
final List<String> roles = user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
SecretKey key = Keys.hmacShaKeyFor(SecurityConstants.JWT_SECRET.getBytes());
final String token = Jwts.builder()
.setHeaderParam("TYP", "JWT")
.setIssuer("moss.example")
.setAudience("you")
.setExpiration(new Date(System.currentTimeMillis() + 1000000))
.setSubject(user.getUsername())
.setIssuedAt(new Date())
.setIssuer("www.moss.example.com")
.setSubject(user.getUsername())
.claim("rol", roles)
.signWith(key)
.compact();
response.setHeader("Authorization", "Bearer " + token);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginData {
private String username;
private String password;
}
11.4 JwtAuthorizationFilter
JwtAuthorizationFilter.java
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
final UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(request);
if (authenticationToken == null) {
chain.doFilter(request, response);
return;
}
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
var token = request.getHeader(SecurityConstants.TOKEN_HEADER);
if (!StringUtils.isEmpty(token) && token.startsWith(SecurityConstants.TOKEN_PREFIX)) {
try {
final Jws<Claims> claimsJws = Jwts.parserBuilder()
.setSigningKey(SecurityConstants.JWT_SECRET.getBytes())
.build()
.parseClaimsJws(token.replace(SecurityConstants.TOKEN_PREFIX, ""));
final String username = claimsJws.getBody().getSubject();
final List<SimpleGrantedAuthority> authorities = ((List<?>)claimsJws.getBody().get("rol")).stream()
.map(authority -> new SimpleGrantedAuthority((String) authority))
.collect(Collectors.toList());
if (!StringUtils.isEmpty(username)) {
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
} catch (ExpiredJwtException e) {
log.warn("Request to parse expired JWT: {} failed: {}", token, e.getMessage());
} catch (UnsupportedJwtException e) {
log.warn("Request to parse unsupported JWT: {} failed: {}", token, e.getMessage());
} catch (MalformedJwtException e) {
log.warn("Request to parse invalid JWT: {} failed: {}", token, e.getMessage());
} catch (SignatureException e) {
log.warn("Request to parse JWT with invalid signature: {} faile: {}", token, e.getMessage());
} catch (IllegalArgumentException e) {
log.warn("Request to parse empty or null JWT: {} failed: {}", token, e.getMessage());
}
}
return null;
}
}
11.5 test on postman
- try to visit GET
http://localhost:8080/api/admin
got 401 error -
POST visit
alhost:8080/api/token
with body{"username":"username","password":"123456"}
,got 200 OK and returned JWT token header, see:Authorization →Bearer eyJUWVAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3d3cubW9zcy5leGFtcGxlLmNvbSIsImF1ZCI6InlvdSIsImV4cCI6MTU5MTUwMDQ1OCwic3ViIjoidXNlcm5hbWUiLCJpYXQiOjE1OTE0OTk0NTh9.0eZRrDcU25ArCVFRdUu2FN2I8YJvAHwDtikqfoSewaY
-
jwtIO verify payload
{ "iss": "www.moss.example.com", "aud": "you", "exp": 1591500458, "sub": "username", "iat": 1591499458 }
- try to visit GET
http://localhost:8080/api/admin
with Headers- KEY:
Authorization
- VALUE:
Bearer eyJUWVAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ3d3cubW9zcy5leGFtcGxlLmNvbSIsImF1ZCI6InlvdSIsImV4cCI6MTU5MTY5MTgyMCwic3ViIjoidXNlcm5hbWUiLCJpYXQiOjE1OTE2OTA4MjAsInJvbCI6WyJST0xFX0FETUlOIl19.64GdIatR22PbFk8ZVeXwO-WXDSa5FJeOb93dWy5afXI
- KEY: