Recently, I encountered a problem in the project: the front-end and back-end are separated, the front-end is done with Vue, all data requests use vue-resource, and no form is used, so the data interaction is used by JSON, the background uses Spring Boot, and permission verification uses Spring Security. Because Spring Security was used before, they processed pages, and this time they simply processed Ajax requests, so they recorded some problems they encountered. The solution here is not only suitable for Ajax requests, but also resolves mobile request verification.
Create a project
First of all, we need to create a Spring Boot project. When creating it, we need to introduce Web, Spring Security, MySQL and MyBatis (the database framework is actually arbitrary, I use MyBatis here). After creation, the dependency file is as follows:
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version></dependency><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><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope></dependency><dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.11</version></dependency>
Note that the last commons-codec dependency was added manually by me. This is an open source project of Apache that can be used to generate MD5 message digests. I will simply process the password in the following text.
Create a database and configure it
In order to simplify the logic, I have created three tables here, namely the user table, the role table, and the user role association table, as follows:
Next, we need to make simple configuration of our database in application.properties. Here we are determined by your specific situation.
spring.datasource.url=jdbc:mysql:///vueblogspring.datasource.username=rootspring.datasource.password=123
Construct entity class
Here mainly refers to constructing user classes. The user classes here are quite special and must implement the UserDetails interface, as follows:
public class User implements UserDetails { private Long id; private String username; private String password; private String nickname; private boolean enabled; private List<Role> roles; @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } @Override public List<GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); for (Role role : roles) { authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName())); } return authorities; } //getter/setter omit...} After implementing the UserDetails interface, there are several methods in this interface that we need to implement. The four methods that return Boolean are all known and known. Enabled indicates whether the period account is enabled. This field does exist in my database. Therefore, according to the query results, the other returns directly for simple periods. The getAuthorities method returns the role information of the current user. The user's role is actually the data in roles. The data in roles is converted to List<GrantedAuthority> and then returned. There is a point to note here. Since the role names I store in the database are all such as 'super administrator', 'ordinary user', etc., and do not start with characters like ROLE_ , so you need to manually add ROLE_ here, remember.
There is also a Role entity class, which is relatively simple and can be created according to the fields of the database. I will not repeat it here.
Create UserService
The UserService here is also quite special, and it is necessary to implement the UserDetailsService interface, as follows:
@Servicepublic class UserService implements UserDetailsService { @Autowired UserMapper userMapper; @Autowired RolesMapper rolesMapper; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { User user = userMapper.loadUserByUsername(s); if (user == null) { //Avoid returning null, here returns a User object that does not contain any value, and verification will also fail in the later password comparison process return new User(); } //Query the user's role information and return to the user. List<Role> roles = rolesMapper.getRolesByUid(user.getId()); user.setRoles(roles); return user; }}After implementing the UserDetailsService interface, we need to implement the loadUserByUsername method in the interface, that is, query the user based on the user name. Two Mappers in MyBatis are injected here, UserMapper is used to query users, and RolesMapper is used to query roles. In the loadUserByUsername method, first query the user according to the passed parameters (the parameter is the user name entered when the user logs in). If the user found is null, you can directly throw a UsernameNotFoundException exception. However, for the convenience of processing, I returned a User object without any value. In this way, during the subsequent password comparison process, you will find that the login failed (here you can adjust it according to your business needs). If the user found is not null, we will query the user's role based on the user id and put the query result into the user object. This query result will be used in the getAuthorities method of the user object.
Security Configuration
Let's take a look at my Security configuration first, and then I'll explain it one by one:
@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserService userService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()); } /** * @param charSequence plaintext* @param s ciphertext* @return */ @Override public boolean matches(CharSequence charSequence, String s) { return s.equals(DigestUtils.md5DigestAsHex(charSequence.toString().getBytes())); } }); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("Super Admin") .anyRequest().authenticated()//Other paths are accessed after logging in.and().formLogin().loginPage("/login_page").successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); out.write("{/"status/":/"ok/",/"msg/":/"Login successfully/"}"); out.flush(); out.close(); } }) .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); out.write("{/"status/":/"error/",/"msg/":/"Login failed/"}"); out.flush(); out.close(); } }).loginProcessingUrl("/login") .usernameParameter("username").passwordParameter("password").permitAll() .and().logout().permitAll().and().csrf().disable(); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/reg"); }}This is the core of our configuration. Please listen to me:
1. First of all, this is a configuration class, so remember to add the @Configuration annotation. Because this is the configuration of Spring Security, you must inherit the WebSecurityConfigurerAdapter.
2. Inject the UserService that has just been created and we will use it later.
3.Configure(AuthenticationManagerBuilder auth) method is used to configure our authentication method, and pass userService in the auth.userDetailsService() method, so that the loadUserByUsername method in the userService will be automatically called when the user logs in. The passwordEncoder behind is optional, either writeable or not, because I generated the user's plaintext password and stored it in the database, so the plaintext password is also needed to process it when logging in, so I added passwordEncoder, and after adding passwordEncoder, I can directly new an anonymous internal class of PasswordEncoder. There are two methods to implement here, and you can know the meaning of the method by looking at the name. The first method encode obviously encrypts the plaintext. Here I use the MD5 message digest. The specific implementation method is provided by commons-codec dependency; the second method matches are password comparison, two parameters, the first parameter is plaintext password, and the second is ciphertext. Here you only need to encrypt the plaintext and compare it with the ciphertext (If you are interested in this, you can continue to consider adding salt to the password).
4.configure(HttpSecurity http) is used to configure our authentication rules, etc. The authorizeRequests method means that the authentication rule configuration is enabled, antMatchers("/admin/**").hasRole("super administrator") means that the path of /admin/** needs to be accessed by users with the 'super administrator' role. I saw on the Internet that my friends have questions about whether to add ROLE_ in the hasRole method. Don't add it here. If you use the hasAuthority method, you need to add it. anyRequest().authenticated() means that all other paths need to be authenticated/logged in before accessing. Next we configured the login page as login_page, the login processing path is /login, the login username is username, and the password is password. We configured these paths to access directly, and log out of login can also be accessed directly, and finally close csrf. In successHandler, use response to return the json that has successfully logged in. Remember not to use defaultSuccessUrl. defaultSuccessUrl is a page that is redirected only after logging in successfully. The same reason is also used for failureHandler.
5. I have configured some filtering rules in the configuration(WebSecurity web) method, so I will not go into details.
6. In addition, for static files, such as /images/** , /css/** , and /js/** , the default is not intercepted.
Controller
Finally, let’s take a look at our Controller, as follows:
@RestControllerpublic class LoginRegController { /** * If you automatically jump to this page, it means that the user is not logged in, and you can return the corresponding prompt* <p> * If you want to support form login, you can judge the type of request in this method, and then decide whether to return to JSON or HTML page* * @return */ @RequestMapping("/login_page") public RespBean loginPage() { return new RespBean("error", "Not logged in yet, please log in!"); }} Overall, this Controller is relatively simple. RespBean returns a simple json, which is not detailed. What you need to pay attention to here is login_page . The login page we configured is a login_page . But in fact, login_page is not a page, but a JSON. This is because when I visit other pages without logging in, Spring Security will automatically jump to the login_page page. However, in Ajax request, this kind of jump is not needed. All I want is a prompt to login or not, so I can return json here.
test
Finally, friends can use tools such as POSTMAN or RESTClient to test login and permission issues, so I won’t demonstrate it.
Ok, after the introduction above, I believe that friends have already understood Spring Boot+Spring Security’s handling of Ajax login requests. Okay, that’s all for this article. If you have any questions, please leave a message to discuss.