To prepare

I want to start this tutorial with a brief overview of the following points.

  • Know the basic concepts of JWT
  • Know Spring Security

In this project, the JWT key uses the user’s own login password, so that each token has a different key and is relatively secure.

General idea:

Login:

  1. POST User name password to \login
  2. The request toJwtAuthenticationFilterIn theattemptAuthentication()Method that takes the POST argument from the request and wraps it into oneUsernamePasswordAuthenticationTokenDelivered to theAuthenticationManagerauthenticate()Method to authenticate.
  3. AuthenticationManagerfromCachingUserDetailsServiceTo find the user information and determine whether the account password is correct.
  4. If the account password is correct, go toJwtAuthenticationFilterIn thesuccessfulAuthentication()Method, we sign and generate tokens back to the user.
  5. If the account password is incorrect, go toJwtAuthenticationFilterIn theunsuccessfulAuthentication()Method, we return an error message to let the user log in again.

Request authentication:

Request to the main idea is that we will take the Authorization from the request field token, if there is no user of this field, Spring Security will default will use AnonymousAuthenticationToken () packaging it, Represents anonymous users.

  1. Arbitrary request initiation
  2. arriveJwtAuthorizationFilterIn thedoFilterInternal()Method for authentication.
  3. If the authentication is successful, we will generateAuthenticationSecurityContextHolder.getContext().setAuthentication()If the value is set to Security, authentication is complete. How to authenticate here is written in our own code, which will be explained later.

Prepare pom. XML

<? The XML version = "1.0" encoding = "utf-8"? > < project XMLNS = "http://maven.apache.org/POM/4.0.0" XMLNS: xsi = "http://www.w3.org/2001/XMLSchema-instance" Xsi: schemaLocation = "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > < modelVersion > 4.0.0 < / modelVersion > < the parent > < groupId > org. Springframework. Boot < / groupId > The < artifactId > spring - the boot - starter - parent < / artifactId > < version > 2.1.7. RELEASE < / version > < relativePath / > <! -- lookup parent from repository --> </parent> <groupId>org.inlighting</groupId> < artifactId > spring - the boot - security - JWT < / artifactId > < version > 0.0.1 - the SNAPSHOT < / version > <name>spring-boot-security-jwt</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <! -- JWT support --> <dependency> <groupId>com.auth0</groupId> <artifactId> Java -jwt</artifactId> <version>3.8.2</version> </dependency> <! -- Cache support --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <! Support --> <dependency> <groupId>org.ehcache</groupId> <artifactId> Ehcache </artifactId> </dependency> <! Javax. cache</groupId> <artifactId>cache-api</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <! <dependency> <groupId>javax.xml.bind</groupId> <artifactId> JAXB-api </artifactId> The < version > 2.3.0 < / version > < / dependency > <! --> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId> JAXB-impl </artifactId> The < version > 2.3.0 < / version > < / dependency > <! --> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId> JAXB -core</artifactId> The < version > 2.3.0 < / version > < / dependency > <! Javax. activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> </dependencies> <build> <plugins> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>Copy the code

The pom.xml configuration file section doesn’t have much to say, but mainly illustrates the following dependencies:

<! <dependency> <groupId>javax.xml.bind</groupId> <artifactId> JAXB-api </artifactId> The < version > 2.3.0 < / version > < / dependency > <! --> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId> JAXB-impl </artifactId> The < version > 2.3.0 < / version > < / dependency > <! --> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId> JAXB -core</artifactId> The < version > 2.3.0 < / version > < / dependency > <! Javax. activation</groupId> <artifactId>activation</artifactId> The < version > 1.1.1 < / version > < / dependency >Copy the code

Because ehCache reads the XML configuration file using these dependencies, which have been optional since JDK 9, users of older versions need to add them to work properly.

Preparation for basic work

The next steps are to create a new entity, simulate a database, and write a JWT utility class.

UserEntity.java

In order to simplify the code, we directly use Security’s existing role class. In the actual project, we must process it ourselves and convert it into the Role class of Security.

public class UserEntity { public UserEntity(String username, String password, Collection<? extends GrantedAuthority> role) { this.username = username; this.password = password; this.role = role; } private String username; private String password; private Collection<? extends GrantedAuthority> role; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Collection<? extends GrantedAuthority> getRole() { return role; } public void setRole(Collection<? extends GrantedAuthority> role) { this.role = role; }}Copy the code

ResponseEntity.java

To facilitate the front end we need to unify the json return format, so define a responseEntity.java.

public class ResponseEntity { public ResponseEntity() { } public ResponseEntity(int status, String msg, Object data) { this.status = status; this.msg = msg; this.data = data; } private int status; private String msg; private Object data; public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public Object getData() { return data; } public void setData(Object data) { this.data = data; }}Copy the code

Database.java

Here we use a HashMap to simulate a database, and the password is pre-encrypted with Bcrypt, which is also the official recommended encryption algorithm of Spring Security (MD5 encryption has been removed in Spring Security 5, which is not secure).

The user name password permissions
jack Jack123 stores Bcrypt after encryption ROLE_USER
danny Danny123 stores Bcrypt after encryption ROLE_EDITOR
smith Smith123 stores Bcrypt after encryption ROLE_ADMIN
@Component public class Database { private Map<String, UserEntity> data = null; public Map<String, UserEntity> getDatabase() { if (data == null) { data = new HashMap<>(); UserEntity jack = new UserEntity( "jack", "$2a$10$AQol1A.LkxoJ5dEzS5o5E.QG9jD.hncoeCGdVaMQZaiYZ98V/JyRq", getGrants("ROLE_USER")); UserEntity danny = new UserEntity( "danny", "$2a$10$8nMJR6r7lvh9H2INtM2vtuA156dHTcQUyU.2Q2OK/7LwMd/I.HM12", getGrants("ROLE_EDITOR")); UserEntity smith = new UserEntity( "smith", "$2a$10$E86mKigOx1NeIr7D6CJM3OQnWdaPXOjWe4OoRqDqFgNgowvJW9nAi", getGrants("ROLE_ADMIN")); data.put("jack", jack); data.put("danny", danny); data.put("smith", smith); } return data; } private Collection<GrantedAuthority> getGrants(String role) { return AuthorityUtils.commaSeparatedStringToAuthorityList(role); }}Copy the code

UserService.java

Here we simulate a service that mimics the operation of a database.

@Service public class UserService { @Autowired private Database database; public UserEntity getUserByUsername(String username) { return database.getDatabase().get(username); }}Copy the code

JwtUtil.java

I wrote a tool class, mainly responsible for JWT signature and authentication.

Public class JwtUtil {private final static Long EXPIRE_TIME = 5 * 60 * 1000; @param username username @param secret user password @return encrypted token */ public static String sign(String) username, String secret) { Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME); try { Algorithm algorithm = Algorithm.HMAC256(secret); return JWT.create() .withClaim("username", username) .withExpiresAt(expireDate) .sign(algorithm); } catch (Exception e) { return null; }} /** * Verify whether the token is correct * @param token key * @param secret User password * @return check */ public static Boolean verify(String) token, String username, String secret) { try { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username", username) .build(); DecodedJWT jwt = verifier.verify(token); return true; } catch (Exception e) { return false; }} public static String getUsername(String token) {try {public static String getUsername(String token) { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { return null; }}}Copy the code

Spring Security reform

To log on to the enticationFilter, we use our custom JwtAuthenticationFilter.

The request for authentication is handled using a custom JwtAuthorizationFilter.

You might think the words look similar, 😜.

UserDetailsServiceImpl.java

We first implement the official UserDetailsService interface, which is mainly responsible for the operation of retrieving data from the database.

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userService.getUserByUsername(username);
        if (userEntity == null) {
            throw new UsernameNotFoundException("This username didn't exist.");
        }
        return new User(userEntity.getUsername(), userEntity.getPassword(), userEntity.getRole());
    }
}
Copy the code

We also need to cache it later, otherwise every request will have to take a data authentication from the database, the database pressure is too big.

JwtAuthenticationFilter.java

The main processing log filter operation, we inherited UsernamePasswordAuthenticationFilter, it would greatly simplify our work.

Public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {/ * filter must set the AuthenticationManager, So here we write it this way, The AuthenticationManager here I'll pass */ public JwtAuthenticationFilter(AuthenticationManager) from the Security configuration The authenticationManager) {/ * run the superclass UsernamePasswordAuthenticationFilter constructor, to set the filter specified method for POST [\ login] * / super (); setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse Response) throws AuthenticationException {// Take the username and password fields from the request POST to log in String username = request.getParameter("username"); String password = request.getParameter("password"); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password); SetDetails (request, token); // setDetails(request, token); // Hand it to AuthenticationManager for authentication return getAuthenticationManager().authenticate(token); } /* If the authentication succeeds, Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { handleResponse(request, response, authResult, null); } /* If the authentication fails, We are here to return to the user name or password error message * / @ Override protected void unsuccessfulAuthentication (it request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { handleResponse(request, response, null, failed); } private void handleResponse(HttpServletRequest request, HttpServletResponse response, Authentication authResult, AuthenticationException failed) throws IOException, ServletException { ObjectMapper mapper = new ObjectMapper(); ResponseEntity responseEntity = new ResponseEntity(); response.setHeader("Content-Type", "application/json; charset=UTF-8"); if (authResult ! User User = (User) authresult.getPrincipal (); String token = JwtUtil.sign(user.getUsername(), user.getPassword()); responseEntity.setStatus(HttpStatus.OK.value()); Responseentity. setMsg(" login successfully "); responseEntity.setData("Bearer " + token); response.setStatus(HttpStatus.OK.value()); response.getWriter().write(mapper.writeValueAsString(responseEntity)); } else {responseEntity.setStatus(httpstatus.bad_request.value ()); Responseentity.setmsg (" username or password error "); responseEntity.setData(null); response.setStatus(HttpStatus.BAD_REQUEST.value()); response.getWriter().write(mapper.writeValueAsString(responseEntity)); }}}Copy the code

Private void handleResponse() is not a very good way to handle this, my idea is to jump to the controller for processing, but the successful authentication token can not be carried over, so I write this first, it is a bit complicated.

JwtAuthorizationFilter.java

Each request to the filter processing, we choose to inherit BasicAuthenticationFilter, considering the Basic authentication and JWT is like, you chose it.

public class JwtAuthorizationFilter extends BasicAuthenticationFilter { private UserDetailsService userDetailsService; Public JwtAuthorizationFilter(AuthenticationManager AuthenticationManager, UserDetailsService userDetailsService) { super(authenticationManager); this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {// Check whether there is a token, Authentication Token = getAuthentication(request); if (token == null) { chain.doFilter(request, response); return; } / / certification success SecurityContextHolder getContext () setAuthentication (token); chain.doFilter(request, response); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { String header = request.getHeader("Authorization"); if (header == null || ! header.startsWith("Bearer ")) { return null; } String token = header.split(" ")[1]; String username = JwtUtil.getUsername(token); UserDetails userDetails = null; try { userDetails = userDetailsService.loadUserByUsername(username); } catch (UsernameNotFoundException e) { return null; } if (! JwtUtil.verify(token, username, userDetails.getPassword())) { return null; } return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); }}Copy the code

SecurityConfiguration.java

Here we configure Security and implement caching. Cache the official we use ready-made CachingUserDetailsService, but the downside is that it has no public methods, we can’t normal instantiation, saving the need curve, the following code has detailed instructions.

/ / open Security @ EnableWebSecurity / / open annotation configuration support @ EnableGlobalMethodSecurity (securedEnabled = true, prePostEnabled = true) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsServiceImpl; // Spring Boot CacheManager, here we use jcache@autoWired Private CacheManager CacheManager; @override protected void configure(HttpSecurity HTTP) throws Exception {// Enable cross-domain http.cors(). And () // security default CSRF .csrf().disable().authorizerequests () // All requests pass by default, but we add security annotations to methods that require permissions. AnyRequest ().permitall ().and() // Add two filters of your own.addFilter(new) JwtAuthenticationFilter(authenticationManager())) .addFilter(new JwtAuthorizationFilter(authenticationManager(), CachingUserDetailsService (userDetailsServiceImpl))) / / front end separation is STATELESS, So the session using this strategy. SessionManagement () sessionCreationPolicy (sessionCreationPolicy. STATELESS); } // Configure AuthenticationManager here, And implement caching @ Override protected void the configure (AuthenticationManagerBuilder auth) throws the Exception {/ / to write your own UserDetailsServiceImpl further wraps, Implement caching CachingUserDetailsService CachingUserDetailsService = CachingUserDetailsService (userDetailsServiceImpl); // jwt-cache we have declared UserCache UserCache = new in ehcache. XML configuration file SpringCacheBasedUserCache(cacheManager.getCache("jwt-cache")); cachingUserDetailsService.setUserCache(userCache); /* security By default, the password will be erased after authentication, but here we use the user's password as the generation key of JWT. If erased, the user's password will not be obtained when JWT is signed. Therefore, automatic password erasure is disabled here. */ auth.eraseCredentials(false); auth.userDetailsService(cachingUserDetailsService); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } / * here we implement caching, we use the ready-made CachingUserDetailsService official, but the class constructor is not public, we can't normal instantiation, so here are curve for national salvation. */ private CachingUserDetailsService cachingUserDetailsService(UserDetailsServiceImpl delegate) { Constructor<CachingUserDetailsService> ctor = null; try { ctor = CachingUserDetailsService.class.getDeclaredConstructor(UserDetailsService.class); } catch (NoSuchMethodException e) { e.printStackTrace(); } Assert.notNull(ctor, "CachingUserDetailsService constructor is null"); ctor.setAccessible(true); return BeanUtils.instantiateClass(ctor, delegate); }}Copy the code

Ehcache configuration

Ehcache 3, JCache, JSR107 standard is used for Ehcache 3. Many online tutorials are based on Ehcache 2, so you may encounter many pits when referring to online tutorials.

JSR107: EMM, in fact JSR107 is a caching standard, as long as each framework complies with this standard, it is the reality of a unified. Basically, I can change the underlying cache system without changing the system code.

Create ehcache. XML file in resources directory:

<ehcache:config xmlns:ehcache="http://www.ehcache.org/v3" xmlns:jcache="http://www.ehcache.org/v3/jsr107"> <ehcache:cache alias="jwt-cache"> <! -- We use the username as the cache key, Java.lang.String</ ehCache :key-type> <ehcache:value-type>org.springframework.security.core.userdetails.User</ehcache:value-type> <ehcache:expiry> <ehcache:ttl unit="days">1</ehcache:ttl> </ehcache:expiry> <! -- number of cache entities --> <ehcache:heap unit="entries">2000</ehcache:heap> </ehcache:cache> </ehcache:config>Copy the code

Enable caching support in application.properties:

spring.cache.type=jcache
spring.cache.jcache.config=classpath:ehcache.xml
Copy the code

Unified Global Exception

We need to unify the return form of the exception, so as to facilitate the front-end call.

We normally use @RestControllerAdvice to unify exceptions, but it can only manage exceptions thrown at the Controller level. Exceptions thrown by Security do not reach Controller and cannot be caught by @RestControllerAdvice, so we need to modify ErrorController.

@RestController
public class CustomErrorController implements ErrorController {

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping("/error")
    public ResponseEntity handleError(HttpServletRequest request, HttpServletResponse response) {
        return new ResponseEntity(response.getStatus(), (String) request.getAttribute("javax.servlet.error.message"), null);
    }
}
Copy the code

test

Try writing a controller, you can also refer to my controller to get user information, I recommend using @authenticationPrincipal annotation!!

@restController public class MainController {// Anyone can access, Check whether the user is legitimate @getMapping ("everyone") public ResponseEntity everyone() {Authentication Authentication = SecurityContextHolder.getContext().getAuthentication(); if (! (authentication instanceof AnonymousAuthenticationToken)) {/ / login user return new ResponseEntity (HttpStatus. OK. The value (), "You are already login", authentication.getPrincipal()); } else { return new ResponseEntity(HttpStatus.OK.value(), "You are anonymous", null); } } @GetMapping("user") @PreAuthorize("hasAuthority('ROLE_USER')") public ResponseEntity user(@AuthenticationPrincipal UsernamePasswordAuthenticationToken token) { return new ResponseEntity(HttpStatus.OK.value(), "You are user", token); } @GetMapping("admin") @IsAdmin public ResponseEntity admin(@AuthenticationPrincipal UsernamePasswordAuthenticationToken  token) { return new ResponseEntity(HttpStatus.OK.value(), "You are admin", token); }}Copy the code

I also use the @isadmin annotation here, which looks like this:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public @interface IsAdmin {
}
Copy the code

This saves writing a long list of @preauthorize () each time, and is more intuitive.

FAQ

How do I resolve JWT expiration issues?

JwtAuthorizationFilter can be added to the JwtAuthorizationFilter. If the user is about to expire, it returns a special status code, and the front end receives this status code to access GET /re_authentication. Carry the old token and GET a new token.

How do I invalidate an issued and unexpired token?

My personal idea is to put the tokens in the cache every time they are generated and take them out of the cache every time they are requested. If not, the cache will be scrapped.

Project address: github.com/Smith-Cruis…

This article was first published on the public account: Java Version of the Web project, welcome to pay attention to get more exciting content