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:
- POST User name password to \login
- The request to
JwtAuthenticationFilter
In theattemptAuthentication()
Method that takes the POST argument from the request and wraps it into oneUsernamePasswordAuthenticationToken
Delivered to theAuthenticationManager
的authenticate()
Method to authenticate. AuthenticationManager
fromCachingUserDetailsService
To find the user information and determine whether the account password is correct.- If the account password is correct, go to
JwtAuthenticationFilter
In thesuccessfulAuthentication()
Method, we sign and generate tokens back to the user. - If the account password is incorrect, go to
JwtAuthenticationFilter
In 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.
- Arbitrary request initiation
- arrive
JwtAuthorizationFilter
In thedoFilterInternal()
Method for authentication. - If the authentication is successful, we will generate
Authentication
用SecurityContextHolder.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