I implemented a simple login function using Spring-Boot. I've done something similar with PHP once, and when I thought I could afford it, I had a hard time, so I'll summarize it as a memorandum. If you write everything comprehensively, you will run out of power, so I will write roughly focusing on the clogged part. I'm wrong here! If you have any questions, please kindly let me know (╹◡╹)
The flow of this process is as follows. There are "hello.html" and "login.html" on the screen, and hello.html is a screen that cannot be seen without logging in. ① Authorization processing If you try to access hello.html without logging in, it will transition to login.html
② Authentication process Compare the "Username" and "Password" values entered in the form with the values stored in the DB. If there is a match, the authentication is successful. If it does not exist, it will transition to login.html and display an error message.
③ Session management If the authentication is successful, the session information is stored in the DB. Allows you to get the username from the session information as needed.
④ Discard the session When the logout button is pressed, the session information will be discarded and authentication will be required again.
The figure below summarizes these processes.
If you implement these at once, your head will get flat, so this time we implemented it in 3 steps.
Below we will look at the implementation steps for each step.
Before that, I will describe the dependencies when creating a project with Spring-Boot.
For more details, see pom.xml.
We will start by making the foundation. Here's what you'll make in this step:
Let's start with all the base configuration files. But before that ...
It seems difficult to tinker with the config file ... So at first I thought Spring-Security was hard to get along with. However, since Spring-Security allows you to write settings in the form of Java classes, I feel that once you write it, you can get a feel for it. I understand intuitively that it is possible to set the property of the class that manages the setting by adding the value (setting for the system) because it feels like playing with the volume and image quality in the game. I think it's easy.
MvcConfig is especially easy to understand, so if you are new to Config, you may want to try it from there. When I first saw the config file, it looked completely different from the code I usually write, so I wrote it before actually looking at the code.
WebSecurityConfig.java
package login.app.config;
import org.springframework.beans.factory.annotation.Autowired;
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.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 login.app.service.UserDetailsServiceImpl;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
//Since the password obtained from the DB to be compared with the form value is encrypted, it is also used to encrypt the form value.
@Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/images/**",
"/css/**",
"/javascript/**"
);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login") //Since the login page does not go through the controller, it is necessary to link it with ViewName.
.loginProcessingUrl("/sign_in") //Submit URL of the form, authentication process is executed when a request is sent to this URL
.usernameParameter("username") //Explicit name attribute of request parameter
.passwordParameter("password")
.successForwardUrl("/hello")
.failureUrl("/login?error")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll();
}
@Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception{
auth
.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER");
}
}
The basic part is carefully summarized by Official, so if you don't know what you are saying, please refer to that. I think it's better to do it.
Now let's take a look at each process.
Read the @Configuration annotation properly as this class is a configuration file! It is for telling Spring. The @EnableWebSecurity annotation that accompanies it is for using this and that of Spring-Security.
This class inherits from the "WebSecurityConfigurerAdapter" class, and it will be used by overriding some methods used in the settings. This time, we will override the configure method and set the authentication / authorization process.
Here, the settings are made so that the request when using static files (image, css, javascript) is not played.
It is a method to describe the setting for the part related to http request in the authentication / authorization process. It seems difficult at first glance because there are a lot of dots lined up, but all I'm doing is setting individual settings, so if I'm fishing for tutorials, I'm sure it will be so. Here is a rough list of what is set.
Here, inMemoryAuthentication () is used to clearly indicate that in-memory authentication is performed, and the user name and password used for authentication are set directly. I think that it is unlikely that modern applications will not use DB, so I would like to revisit the authentication process in the part that uses DB.
If you look at tutorials, you will often see the method name configureGlobal and the @Autowired annotation. Speaking of @Autowired, you often see it used for fields, but you can also attach it to methods. Regarding the config class this time, the name can be set to configure for the method without @Autowired annotation, and any name can be set for the method with @Autowired annotation. Reference
Next, let's look at the configuration file related to ViewName.
MvcConfig.java
package login.app.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
/**
* 「/login from the URL "login".Call html
*/
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
}
Normally, in Spring-Boot, there is a controller, and after picking up the request with @RequesMapping annotation and doing some processing, it returns the view name for the screen (html) and displays the screen. However, since Spring-Security teacher is doing the login process, I think that there is no problem even if you do not implement LoginController or something like that. However, since it is necessary to separate the association between the view name and the URL, we will describe it in this configuration file. Here, when a request comes with the URL "/ login", the process is described by displaying "login" with the view name (login.html). I would be very happy if you could tell me if there is a better way to describe it.
I'll omit hello.html because it's just a greeting, but I'll write about login.html because it's a little addictive.
login.html
<form th:action="@{/sign_in}" method="post">
<input type="text" name="username">user_name
<br/>
<input type="password" name="password">password
<br/>
<input type="submit" value="Login">
</form>
The point to note here is that the value of the name attribute should match the one described in the configuration file, and that the destination of the form should be specified by the th: action attribute. However, if you write "/ sign_in", you will get angry that there is no such URL. This is because Spring-Security's CSRF measures are enabled by default. If you pass the plain URL to acrtion without thinking in this state, you will get angry because the token for CSRF countermeasures is not placed. There are various ways to solve this, but this time, by setting "th: action = @ {URL}" in Thymeleaf, a token will be generated and put on the URL. Reference
Also, by default, the URL of the destination is a long URL that performs authentication processing, but I think it is better to specify it in the configuration file.
If you execute it with the implementation so far, the login screen will be displayed, so if you enter "user" and "password", you will be able to transition to hello.html. Since I forgot to capture the execution result every time, the execution screen will be displayed only in step 3, but please forgive me.
We have implemented a feature that allows only the user logged in in step 1 to see the page. However, it is not possible to identify "who logged in" if everyone uses a common user name and password. Security is too ridiculous in the first place. In this step, we will add a function that only users registered in the DB can log in. The following items are required.
Since the purpose here is to create a login function, I will omit how to use Entity and EntityManager, but I would like to summarize this area as well. So let's take a look at the UserDetailsServiceImpl class.
UserDetailsServiceImpl.java
package login.app.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import login.app.dao.LoginUserDao;
import login.app.entity.LoginUser;
@Service
public class UserDetailsServiceImpl implements UserDetailsService{
//A class that implements a method to search user information from DB
@Autowired
private LoginUserDao userDao;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
LoginUser user = userDao.findUser(userName);
if (user == null) {
throw new UsernameNotFoundException("User" + userName + "was not found in the database");
}
//List of permissions
//There are Admin, User, etc., but since we will not use them this time, only USER is temporarily set.
//When using authority, it is necessary to create and manage the authority table and user authority table on the DB.
List<GrantedAuthority> grantList = new ArrayList<GrantedAuthority>();
GrantedAuthority authority = new SimpleGrantedAuthority("USER");
grantList.add(authority);
//RawData password cannot be passed, so encryption
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//Since UserDetails is an interface, cast the user object created by the constructor of the User class.
UserDetails userDetails = (UserDetails)new User(user.getUserName(), encoder.encode(user.getPassword()),grantList);
return userDetails;
}
}
The processing flow of the method loadUserByUsername that acquires the information for performing the authentication process is as follows.
I would like to take a look at each of them.
In Spring-Security, it is mandatory to pass the authority (Admin, User, etc.) to the authentication information. However, since all you need is a User here, we will temporarily generate permission information from SimpleGrantedAuthority and put it in the GrantedAuthority list. If you want to set the authority, you need to create the authority table and user authority table in the DB and increase the number of entities, but this time it is OK if you can log in, so I will omit it.
In Spring-Security, passwords are basically encrypted. Since I have no knowledge about encryption, I feel that I am using famous places for the time being, but when making an actual application, I have knowledge about each method and encrypt the password at the stage of storing user information in the DB. I think it will be necessary.
In Spring-Security, instead of passing user information as user Entity, a user name called UserDetails, encrypted password, and UserDetails type object created by permission information are passed to the authentication process. Since the User class is an implementation class of the UserDetails interface, it can be used as a return value as it is, but I think it is better to cast it according to the method type and then return it.
Next, since the authentication information has been changed from in-memory to DB, I would like to take a look at the changed part of the configuration file.
WebSecurityConfig.java(Authentication processing part)
public void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
Here, if you specify that userDetailsService is used for authentication and pass the implementation class created earlier as an argument, a class called DaoAuthenticationProvider will be called and the loadUserByUsername method implemented during authentication will be called. .. Specify the same encryption method as that of DB in passwordEncoder, and use it to compare the input value of the form with the password of DB and perform the authentication process.
If you submit the form with login.html again with the implementation so far, it will search the DB and only the users existing in the DB will be authenticated. You did it.
Finally, it manages session information and realizes the following functions.
There are various methods of session management, but this time I will put it on MySQL. However, all you have to do is prepare a [session table generation query](https://github.com/spring-projects/spring-session/tree/master/spring-session-jdbc/src/main/ Just run resources / org / springframework / session / jdbc) and mess around with application.properties and dependencies. Spring-Security will take care of the contents.
Session management may not be able to be written here, such as using Redis with NoSQL or what to do with the session ID, so this time I will only write a rough usage method for creating a login function, and detailed contents I would like to look at another one.
The creation itself is OK if you execute the CREATE statement for each SQL you are using at the above link. You can see how the session behaves in the app by sequentially SELECTing the contents of this table. I need to know that area properly, so I'd like to organize it a little more before writing.
Next is the setting to use the table, but this is OK just by adding a little to application.properties and pom.xml. Thank you.
application.properties
spring.session.store-type=jdbc
pom.xml(Session usage part)
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
I don't think pom.xml is in the list of dependencies at the project generation stage, so you need to add it by hand. I didn't understand this and it was quite packed.
"Hello, user name san!" In hello.html in order to see something like, who you are logged now, you must have information that. This will be implemented by the init method of HelloController that transitions when login is successful.
HelloController.java(Method called at screen transition)
@RequestMapping("/hello")
private String init(Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
//Get login user information from Principal
String userName = auth.getName();
model.addAttribute("userName", userName);
return "hello";
}
The point here is the SecurityContextHolder. This stores the information of the current logged-in user, but it is actually an HttpSession. In other words, the user information contained in the session information defined on Spring-Security is used as a key to refer to the DB that stores the session information, and the login status of the corresponding user is set. It's complicated. I will organize this area properly and summarize it in another article (o_o) If it is the original application, we will pick up the user ID area from the DB from the acquired user name and read the information of each user further ..., but this time we just put the user name on the request scope. I will keep it in. By transitioning to hello.html in this state, the name of the logged-in user can be displayed on the screen.
At the time of logout, there are various things such as discarding session information, but if you send a request to "/ logout" of Spring-Security teacher, it will do it. You can implement the logout process yourself, but it seems that CSRF will do all this well, so I think it's safe to leave it to us. However, I think it is necessary to keep in mind what is required in the process and how the DB information of the session changes. Organize that area (abbreviated below)
There is one caveat, it seems that when you transition to "/ logout" on hello.html, you must always transition with the "POST" method. It seems that it is for doing this and that of CSRF, but since my understanding is fluffy, I would like to do my best to study security.
With this, we have implemented a set of functions. Finally, I would like to put the execution result of a series of processes.
The source code for this series of implementations is listed on GitHub. There is also a sql file in the sql folder to do this and that with a single command, so if you don't mind.
Recommended Posts