Protect your Spring Boot application with JWT - Spring Security Actual Warfare
Author freewolf
Please indicate the origin of the original article for reprinting
Key word
Spring Boot
,OAuth 2.0
,JWT
,Spring Security
,SSO
,UAA
Written in front
Recently, I quieted down and relearned some things. I hardly wrote any code in the last year. The days of exhaustion are over at last. It's good to sit down, get a cup of coffee and think about some questions. These days I have been asked about Spring Boot's implementation of OAuth certification with Spring Security. I wrote a Demo and share it with you. Spring 2 has not used Java since, mainly because xml is too troublesome, so I put myself into Node.js. Now Java is much better than before, no matter the efficiency of execution or other things. Thank Pivotal team for their efforts on Springboot and Josh Long, an interesting city lion.
I also do Java to toss about micro services, because at present, it is best to find Java programming apes in China. Although the level is good, it is difficult to find, at least, unlike other programming languages, it is really difficult to find a person who will be the best programming language in the world, PHP.
Spring Boot
With artifacts like Spring Boot, you can easily use a powerful Spring framework. The only thing you need to care about is creating applications, not configuring them anymore, "Just run!" That's what Josh Long has to say every time he speaks, and the other thing he has to say is "make jar not war", which means that you don't have to care too much about Tomcat, Jetty or Undertow. It's a good thing to concentrate on solving logical problems. Deployment is much simpler.
Creating Spring Boot Applications
There are many ways to create Spring Boot projects, which are also recommended by the authorities:
start.spring.io makes it easy to choose the components you want to use, and command-line tools do, of course. At present Spring Boot has reached 1.53, I am too lazy to update dependencies and continue to use version 1.52. Although Ali also has a domestic version of the central treasury, it is not known whether it is stable or not. If you are interested, you can try it on your own. You can choose Maven or Gradle as the building tool for your project. Gradle is more elegant and uses Groovy to describe it.
Open start.spring.io, and create a project that requires only one Dependency, the Web, then download the project and open it with IntellJ IDEA. My Java version is 1.8.
Here's a look at the dependencies in the pom.xml file for the entire project:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
All Spring Boot-related dependencies come in the form of starter, so you don't need to care about versions and related dependencies, so this greatly simplifies the development process.
When you integrate the spring-boot-maven-plugin plug-in into the pom file, you can use Maven-related commands to run your application. For example, mvn spring-boot:run, which launches an embedded Tomcat and runs on port 8080, will give you a Whitelabel Error Page if you visit it directly, which means Tomcat has started.
Create a Web application
This is still an article on Web security, but you have to have a simple HTTP request response first. Let's first get a Controller that can return to JSON. Modify the entry file of the program:
@SpringBootApplication @RestController @EnableAutoConfiguration public class DemoApplication { // main function, Spring Boot program entry public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } // The root directory map Get access directly returns a string @RequestMapping("/") Map<String, String> hello() { // Returning to map becomes JSON key value Map<String,String> map=new HashMap<String,String>(); map.put("content", "hello freewolf~"); return map; } }
Here I try to make it clear that people who don't know Spring Security can understand this thing through this example. Many people think it's very complicated, but to Apache Shiro, in fact, this is not difficult to understand. It's good to know the main processing flow and which classes play a role in the process.
The biggest benefit of Spring Boot for developers is that it can automatically configure Spring applications. Spring Boot automatically configures the Spring framework based on the third-party dependencies declared in the application, without requiring explicit declarations. Spring Boot recommends Java annotation-based configuration rather than traditional XML. Autoconfiguration can be enabled by simply adding the @EnableAutoConfiguration annotation on the main configuration Java class. Spring Boot's automatic configuration function is not intrusive, but is implemented as a basic default.
For this entry class, we add @RestController and @Enable AutoConfiguration annotations.
The @RestController annotation is equivalent to @ResponseBody and @Controller combined.
run the whole project. Visit http://localhost:8080/, and you can see the output of this JSON. Using Chrome browser, you can install the JSON Formatter plug-in to display more PL.
{ "content": "hello freewolf~" }
In order to show a unified JSON return, a JSONResult class is established here for simple processing. First, modify pom.xml and add the dependencies of org.json.
<dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> </dependency>
Then we add a new class to our code, which has only one result set processing method, because it's only Demo, all of which are in a file. This class only makes the returned JSON result into three parts:
Status - Return status code 0 represents normal return, the rest are errors
Message - General display error message
Result-result set
class JSONResult{ public static String fillResultString(Integer status, String message, Object result){ JSONObject jsonObject = new JSONObject(){{ put("status", status); put("message", message); put("result", result); }}; return jsonObject.toString(); } }
Then we introduce a new @RestController and return some simple results. Later, we will control access to these contents. We use the result set processing class above. Here are two more methods. Later, we will test the validation of permissions and roles.
@RestController class UserController { // Routing maps to / users @RequestMapping(value = "/users", produces="application/json;charset=UTF-8") public String usersList() { ArrayList<String> users = new ArrayList<String>(){{ add("freewolf"); add("tom"); add("jerry"); }}; return JSONResult.fillResultString(0, "", users); } @RequestMapping(value = "/hello", produces="application/json;charset=UTF-8") public String hello() { ArrayList<String> users = new ArrayList<String>(){{ add("hello"); }}; return JSONResult.fillResultString(0, "", users); } @RequestMapping(value = "/world", produces="application/json;charset=UTF-8") public String world() { ArrayList<String> users = new ArrayList<String>(){{ add("world"); }}; return JSONResult.fillResultString(0, "", users); } }
Re run this file and visit http://localhost:8080/users to see the following results:
{ "result": [ "freewolf", "tom", "jerry" ], "message": "", "status": 0 }
If you're careful, you'll find that when JSON returns here, Chrome's formatting plug-in doesn't seem to recognize it? Why is that? Let's take a look at the Header information of the two methods we wrote with curl.
curl -I http://127.0.0.1:8080/ curl -I http://127.0.0.1:8080/users
You can see the first method, hello, because the return value is Map < String, String >, Spring has a mechanism to automatically process it into JSON:
Content-Type: application/json;charset=UTF-8
The second method, usersList, because of String when it returns, because @RestControler already contains @ResponseBody, which is to return content directly, is not a template. So it is:
Content-Type: text/plain;charset=UTF-8
So how to make it JSON? In fact, it's very simple, just need to add some annotations.
@RequestMapping(value = "/users", produces="application/json;charset=UTF-8")
That's all right.
Protect your Spring Boot application with JWT
Finally, let's start with the topic. Here we will control access to / users. First we can apply for a JWT(JSON Web Token reads jot), and then we can get the data through this access / users.
With regard to JWT, going out and heading for the following is beyond the scope of this article:
JWT is still a new technology to a large extent. It calculates information digests by using HMAC(Hash-based Message Authentication Code) and can also be signed by using the private key in RSA public and private keys. This is selected according to the business scenario.
Add Spring Security
According to what we said above, we need access control for / users, allowing users to log in at / login and get Token. Here we need to add spring-boot-starter-security to pom.xml. After joining, our Spring Book project will need to provide authentication. The relevant pom. XML is as follows:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
So far, all our previous routes need authentication. We will introduce a security settings class, WebSecurity Config, which needs to be inherited from the WebSecurity ConfigurerAdapter class.
@Configuration @EnableWebSecurity class WebSecurityConfig extends WebSecurityConfigurerAdapter { // Setting up HTTP validation rules @Override protected void configure(HttpSecurity http) throws Exception { // Close csrf validation http.csrf().disable() // Authenticate requests .authorizeRequests() // All/all requests are granted .antMatchers("/").permitAll() // All / login POST requests are released .antMatchers(HttpMethod.POST, "/login").permitAll() .antMatchers("/hello").hasAuthority("AUTH_WRITE") .antMatchers("/world").hasRole("ADMIN") // All requests require authentication .anyRequest().authenticated() .and() // Add a filter and submit all access / login requests to JWTLoginFilter to process this class for all JWT-related content .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class) // Add a filter to verify the validity of Token for other requests .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // Using custom authentication components auth.authenticationProvider(new CustomAuthenticationProvider()); } }
First put two basic classes, one is responsible for storing username and password, the other is a permission type, responsible for storing permissions and roles.
class AccountCredentials { private String username; private String password; 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; } } class GrantedAuthorityImpl implements GrantedAuthority{ private String authority; public GrantedAuthorityImpl(String authority) { this.authority = authority; } public void setAuthority(String authority) { this.authority = authority; } @Override public String getAuthority() { return this.authority; } }
In the security settings class above, we set up access / login for everyone and POST mode, and any other routing needs authentication. Then all requests for access / login are handed over to the JWTLoginFilter filter for processing. Later we will create this filter and other JWT Authentication Filter and Custom Authentication Provider classes that we need here.
First, create a class for JWT generation and signature verification.
class TokenAuthenticationService { static final long EXPIRATIONTIME = 432_000_000; // 5 days static final String SECRET = "P@ssw02d"; // JWT password static final String TOKEN_PREFIX = "Bearer"; // Token prefix static final String HEADER_STRING = "Authorization";// Header Key for Token // JWT Generation Method static void addAuthentication(HttpServletResponse response, String username) { // Generate JWT String JWT = Jwts.builder() // Save permissions (roles) .claim("authorities", "ROLE_ADMIN,AUTH_WRITE") // User Name Writes to Title .setSubject(username) // Validity period setting .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME)) // Signature settings .signWith(SignatureAlgorithm.HS512, SECRET) .compact(); // Write JWT to body try { response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_OK); response.getOutputStream().println(JSONResult.fillResultString(0, "", JWT)); } catch (IOException e) { e.printStackTrace(); } } // JWT Verification Method static Authentication getAuthentication(HttpServletRequest request) { // Get token from Header String token = request.getHeader(HEADER_STRING); if (token != null) { // Parsing Token Claims claims = Jwts.parser() // Check sign .setSigningKey(SECRET) // Remove Bearer .parseClaimsJws(token.replace(TOKEN_PREFIX, "")) .getBody(); // User name String user = claims.getSubject(); // Get permissions (roles) List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities")); // Return the authentication token return user != null ? new UsernamePasswordAuthenticationToken(user, null, authorities) : null; } return null; } }
This class consists of two static methods, one responsible for generating JWT and the other for authenticating JWT and finally generating authentication tokens. The annotations have been clearly written, so I won't say much here.
Here we look at the custom validation component, which is simply written here. This class provides password validation function. In practical use, it is replaced by its own validation logic, which extracts, compares and gives users corresponding privileges from the database.
// Custom Authentication Component class CustomAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // Get the Authenticated User Name & Password String name = authentication.getName(); String password = authentication.getCredentials().toString(); // Authentication logic if (name.equals("admin") && password.equals("123456")) { // Here set permissions and roles ArrayList<GrantedAuthority> authorities = new ArrayList<>(); authorities.add( new GrantedAuthorityImpl("ROLE_ADMIN") ); authorities.add( new GrantedAuthorityImpl("AUTH_WRITE") ); // Generate token Authentication auth = new UsernamePasswordAuthenticationToken(name, password, authorities); return auth; }else { throw new BadCredentialsException("Password error~"); } } // Can I provide authentication services of input type? @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } }
The following implementation of JWTLogin Filter is relatively simple, except that the constructor needs to rewrite three methods.
attemptAuthentication - Called when authentication is required for login
successfulAuthentication - Called after successful verification
Unsuccessful Authentication - Called after failed validation, where 500 error returns are injected directly, and HTTP returns 200 because the same JSON returns
class JWTLoginFilter extends AbstractAuthenticationProcessingFilter { public JWTLoginFilter(String url, AuthenticationManager authManager) { super(new AntPathRequestMatcher(url)); setAuthenticationManager(authManager); } @Override public Authentication attemptAuthentication( HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException { // JSON deserialization to AccountCredentials AccountCredentials creds = new ObjectMapper().readValue(req.getInputStream(), AccountCredentials.class); // Returns a validation token return getAuthenticationManager().authenticate( new UsernamePasswordAuthenticationToken( creds.getUsername(), creds.getPassword() ) ); } @Override protected void successfulAuthentication( HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException { TokenAuthenticationService.addAuthentication(res, auth.getName()); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { response.setContentType("application/json"); response.setStatus(HttpServletResponse.SC_OK); response.getOutputStream().println(JSONResult.fillResultString(500, "Internal Server Error!!!", JSONObject.NULL)); } }
Complete the last class, JWT Authentication Filter, which is also an interceptor that intercepts all requests requiring JWT, and then invokes the static method of the TokenAuthentication Service class to do JWT validation.
class JWTAuthenticationFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { Authentication authentication = TokenAuthenticationService .getAuthentication((HttpServletRequest)request); SecurityContextHolder.getContext() .setAuthentication(authentication); filterChain.doFilter(request,response); } }
Now the code is finished, the whole Spring Security combined with JWT is almost the same, let's test it and talk about the overall process.
Start testing and run the whole project first. Here's the process:
Pre-program Start-main Function
Register Validation Components - the WebSecurity Config class configure (Authentication Manager Builder auth) method, where we register custom validation components
Setting up validation rules - the WebSecurity Config class configure(HttpSecurity http) method, where various routing access rules are set
Initialization filter components - JWTLoginFilter and JWTAuthenticationFilter classes are initialized
First, test to get Token, which is tested using the CURL command line tool.
curl -H "Content-Type: application/json" -X POST -d '{"username":"admin","password":"123456"}' http://127.0.0.1:8080/login
Result:
{ "result": "eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ", "message": "", "status": 0 }
Here we get the relevant JWT, after anti-Base64, which is the following content, standard JWT.
{"alg":"HS512"}{"authorities":"ROLE_ADMIN,AUTH_WRITE","sub":"admin","exp":1493782240}ͽ]BS`pS6~hCVH% ܬ)֝ଖoE5р
The whole process is as follows:
Get the incoming JSON and parse the username password - JWTLoginFilter class attemptAuthentication method
Custom Authentication Provider Class Authentication Method
Yancheng Success-JWTLoginFilter Class Successful Authentication Method
Generating the JWT - TokenAuthentication Service class addAuthentication method
Test another access resource:
curl -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sQVVUSF9XUklURSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDkzNzgyMjQwfQ.HNfV1CU2CdAnBTH682C5-KOfr2P71xr9PYLaLpDVhOw8KWWSJ0lBo0BCq4LoNwsK_Y3-W3avgbJb0jW9FNYDRQ" http://127.0.0.1:8080/users
Result:
{ "result":["freewolf","tom","jerry"], "message":"", "status":0 }
It shows that our Token can be accessed normally when it comes into effect. You can test other results yourself. Back to the process flow:
Interception upon request - Method in JWTAuthenticationFilter
Verify the getAuthentication method of the JWT - TokenAuthentication Service class
Access Controller
So the main process of this article is over. This article mainly introduces how to use Spring Security and JWT to protect your Spring Boot application. How to use Role and Authority? In Spring Security, there is no distinction between Role and Authority for the implementation class of Granted Authority interface. The difference between them is that if hasAuthority judges, it is to judge the whole string. When HasRole is judged, the system automatically adds ROLE_to the judged Role string, that is, hasRole("CREATE") and Auth. Ority ('ROLE_CREATE') is the same. So far in this article, you have used the knowledge points introduced in this article.
I'll upload the code to Github after I've cleaned it up.
Code in this article
https://github.com/freew01f/s...