RBAC Done Right: Spring Security, JWT, JSON Login, and BCrypt
Title: Universal and versatile! That's how a permission system should be designed. Author: 皮皮林551 Tags: Backend
Table of Contents
- RBAC Permission Analysis
- Spring Security Simple Usage
- Spring Security with JWT
- Spring Security JSON Login
- Spring Security Password Encryption
- Database Authentication
- Summary
RBAC Permission Analysis
RBAC stands for Role-Based Access Control. This section explains RBAC from the perspectives of what RBAC is, model categories, what a permission is, user group usage, and example analysis.
Mind map
What is RBAC
RBAC is user-role permission control. It associates users with roles, and roles with permissions, thereby indirectly granting permissions to users, as shown below.
For typical systems, many users share the same permissions. When assigning permissions, you must assign them to each specific user, and modifications require updating those users one by one. With roles, you only need to modify the role to change permissions. This improves efficiency and reduces permission vulnerabilities.
Model Categories
RBAC models are divided into RBAC0, RBAC1, RBAC2, and RBAC3. The most commonly used is RBAC0.
- RBAC0
RBAC0 is the simplest model and includes two variants:
- Users and roles have a many-to-one relationship: one user has one role, while a role can have many users.
- Users and roles have a many-to-many relationship: one user can have multiple roles, and a role can have many users.
For a system with a single function and few people, consider: Zhang San is both an administrator and handles finance, so he holds two permissions.
- RBAC1
Compared to RBAC0, this adds sub-roles and introduces inheritance.
- RBAC2 Model
RBAC2 adds constraints and functionality on top of RBAC0.
Role mutual exclusion: A user cannot hold two mutually exclusive roles. For example, in a financial system, a user cannot have both accountant and auditor roles.
Cardinality constraint: A role has a fixed number of members. For instance, the CEO role can have only one user.
Prerequisite: To obtain a higher role, the user must first have a lower-level role. For example, to be a manager, you must first have deputy general manager permissions; the deputy role is a prerequisite.
Runtime mutual exclusion: A user can have two roles but cannot use both simultaneously; they must switch roles. For example, the roles of general manager and specialist can only be active one at a time.
RBAC3 Model
This is the unified model combining RBAC1 and RBAC2.
What is a Permission
Permission is a collection of resources. Resources refer to all content in the software: page operation permissions, page access permissions, and data CRUD permissions. For example, a system has pages for plan management, customer management, contract management, inventory notice management, food security traceability, grain statistics query, and equipment management. Access to these pages and whether the menu is visible are all permissions.
User Group Usage
User groups divide many users into a group and grant roles in bulk, i.e., grant permissions in bulk. For example, a department has 10,000 employees with the same role. Without user groups, you would need to grant roles one by one. With a group, you assign the role to the group once, effectively granting it to all users.
Advantages: reduced workload, easier understanding, and support for multi-level management.
Spring Security Simple Usage
First, add the dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Then add an endpoint:
package com.example.demo.web;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class Test {
@RequestMapping("/test")
public String test(){
return "test";
}
}
Start the project and find the generated password in the logs.
Access the endpoint and see the login page.
Enter username and password:
Username: user
Password: 984cccf2-ba82-468e-a404-7d32123d0f9c
Login successful
Add custom username and password in the configuration file:
spring:
security:
user:
name: ming
password: 123456
roles: admin
Enter those credentials to log in normally.
In-memory authentication: create a class extending WebSecurityConfigurerAdapter:
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("123").roles("admin");
}
}
This configures username admin, password 123, role admin.
HttpSecurity
Intercept methods:
package com.ming.demo.interceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("itguang").password("123456").roles("USER").and()
.withUser("admin").password("{noop}" + "123456").roles("ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().permitAll()
.and()
.formLogin()
.permitAll()
.and()
.logout()
.permitAll();
}
}
This configures interception of all method accesses.
Spring Security with JWT
This is a small demo: after login, return a JWT token.
Add dependencies
Add web dependency.
Add JWT and Security dependencies:
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
Create a JwtUser implementing UserDetails
Create a JavaBean:
package com.example.demo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
public class JwtUser implements UserDetails {
private String username;
private String password;
private Integer state;
private Collection<? extends GrantedAuthority> authorities;
public JwtUser(){}
public JwtUser(String username, String password, Integer state, Collection<? extends GrantedAuthority> authorities){
this.username = username;
this.password = password;
this.state = state;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Write a utility class to generate tokens
Write a utility class to generate, refresh, and validate tokens:
package com.example.demo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtTokenUtil implements Serializable {
private String secret;
private Long expiration;
private String header;
private String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + expiration);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
}
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(2);
claims.put("sub", userDetails.getUsername());
claims.put("created", new Date());
return generateToken(claims);
}
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put("created", new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
public Boolean validateToken(String token, UserDetails userDetails) {
JwtUser user = (JwtUser) userDetails;
String username = getUsernameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
}
Write an interceptor
Write a Filter to check JWT:
package com.example.demo;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String authHeader = httpServletRequest.getHeader(jwtTokenUtil.getHeader());
if (authHeader != null && StringUtils.isNotEmpty(authHeader)) {
String username = jwtTokenUtil.getUsernameFromToken(authHeader);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authHeader, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
Write the UserDetailsService implementation
Implement the verification process:
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.management.relation.Role;
import java.util.List;
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userMapper.selectByUserName(s);
if (user == null) {
throw new UsernameNotFoundException(String.format("'%s'.this user does not exist", s));
}
List<SimpleGrantedAuthority> collect = user.getRoles().stream().map(Role::getRolename).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return new JwtUser(user.getUsername(), user.getPassword(), user.getState(), collect);
}
}
Write the login logic
The login method returns a JWT token:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
public User findByUsername(String username) {
User user = userMapper.selectByUserName(username);
return user;
}
public RetResult login(String username, String password) throws AuthenticationException {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
final Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return new RetResult(RetCode.SUCCESS.getCode(), jwtTokenUtil.generateToken(userDetails));
}
}
Finally configure the Config
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and().headers().cacheControl();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
registry.requestMatchers(CorsUtils::isPreFlightRequest).permitAll();
}
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
final CorsConfiguration cors = new CorsConfiguration();
cors.setAllowCredentials(true);
cors.addAllowedOrigin("*");
cors.addAllowedHeader("*");
cors.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", cors);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}
Run and get the token
After running, the result returns a token.
Spring Security JSON Login
Here we configure JSON login in Spring Security.
We need to override the UsernamePasswordAuthenticationFilter class and configure Spring Security.
Override UsernamePasswordAuthenticationFilter
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//attempt Authentication when Content-Type is json
if(request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
||request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
//use jackson to deserialize json
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken authRequest = null;
try (InputStream is = request.getInputStream()){
AuthenticationBean authenticationBean = mapper.readValue(is,AuthenticationBean.class);
authRequest = new UsernamePasswordAuthenticationToken(
authenticationBean.getUsername(), authenticationBean.getPassword());
}catch (IOException e) {
e.printStackTrace();
authRequest = new UsernamePasswordAuthenticationToken(
"", "");
}finally {
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
//transmit it to UsernamePasswordAuthenticationFilter
else {
return super.attemptAuthentication(request, response);
}
}
}
Configure SecurityConfig
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.antMatcher("/**").authorizeRequests()
.antMatchers("/", "/login**").permitAll()
.anyRequest().authenticated()
//Note: formLogin() is required, otherwise the original UsernamePasswordAuthenticationFilter won't appear, and we can't configure our custom filter.
.and().formLogin().loginPage("/")
.and().csrf().disable();
//Replace the original UsernamePasswordAuthenticationFilter with our custom filter
http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
//Register the custom UsernamePasswordAuthenticationFilter
@Bean
CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
filter.setAuthenticationSuccessHandler(new SuccessHandler());
filter.setAuthenticationFailureHandler(new FailureHandler());
filter.setFilterProcessesUrl("/login/self");
//This is key: reuse the AuthenticationManager configured in WebSecurityConfigurerAdapter; otherwise you must assemble AuthenticationManager yourself.
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
This completes JSON login for Spring Security.
Spring Security Password Encryption
Configure the following in the Config class:
/**
* Password encryption
*/
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
Use this method to encrypt passwords in the service layer:
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Resource
private UserRepository userRepository;
@Resource
private BCryptPasswordEncoder bCryptPasswordEncoder; //inject bcrypt encryption
@Override
public User add(User user) {
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword())); //encrypt password
User user2 = userRepository.save(user);
return user2;
}
@Override
public ResultInfo login(User user) {
ResultInfo resultInfo=new ResultInfo();
User user2 = userRepository.findByName(user.getName());
if (user2==null) {
resultInfo.setCode("-1");
resultInfo.setMessage("Username does not exist");
return resultInfo;
}
//Check password
if (!bCryptPasswordEncoder.matches(user.getPassword(),user2.getPassword())) {
resultInfo.setCode("-1");
resultInfo.setMessage("Incorrect password");
return resultInfo;
}
resultInfo.setMessage("Login successful");
return resultInfo;
}
}
So, BCryptPasswordEncoder is used to encrypt passwords before storing in the database.
Database Authentication
Here we use database authentication with Spring Security.
Design the database tables
Design the database tables as shown.
Focus on configuring SpringConfig
@Configurable
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService; // inject service layer
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Pass the Service for verification
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login").permitAll()
.and()
.csrf().disable();
}
}
Here, the focus is on configuring SpringConfig.
Summary
This article focuses on RBAC permission configuration, simple Spring Security usage, using Spring Security + JWT for front-end/back-end separation, configuring JSON login, and password encryption methods.