[JAVA] [Reverse lookup] Spring Security (updated from time to time)

Overview

When using Spring Security, we have summarized it in the form of reverse lookup. The answer here is not the correct answer, it is just one method, so I hope you can refer to it when you are unsure about implementation.

The reference will be updated from time to time.

environment

item version
Java 8
Spring Boot 2.2.4

Preparation

Below is the minimum required code.

Limit all requests



@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated();
    }
}

Since we will not connect to the DB this time, we will use the following authentication information.

application.yml


spring:
    security:
        user:
            name: user
            password: pass
            roles: USER

Notes

--(A), (B), etc. appear in the code, but this means that the same thing will be included. The scope is only on each reference. --The controller class for screen display (`` `@ Controller```) will be omitted, so please create it as appropriate.

Reverse resolution

Common edition

I don't want to limit certain paths

SecurityConfig.java



@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/","/login") // 「/」「/"login" can be accessed without authentication
                .permitAll()
                .anyRequest()
                .authenticated();
    }
}

I want to disable CSRF

CSRF is enabled by default.

SecurityConfig.java



@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated();
        http.csrf().disable();
    }
}

I want to accept requests from outside (sites with different origins)

SecurityConfig.java



@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated();
        http.cors().configurationSource(getCorsConfigurationSource());
    }

    private CorsConfigurationSource getCorsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        //Allow all methods
        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
        //Allow all headers
        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
        //Allow all origins
        corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL);

        UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
        //Can be set for each path. Here set for all paths
        corsSource.registerCorsConfiguration("/**", corsConfiguration);

        return corsSource;
    }
}

I want to customize the authentication process

authenticationproviderCreate an implementation class for.

CustomeAuthenticationProvider.java


@Configuration
public class CustomeAuthenticationProvider implements AuthenticationProvider {

    //Authentication process
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        if (!"user".equals(username) || !"password".equals(password)) {
            throw new BadCredentialsException("Incorrect login information");
        }
        return new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

}

securityconfigSpecify the provider created above in.

SecurityConfig.java


@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomeAuthenticationProvider authenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated();

        http.formLogin();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //Specify the Provider created above
        auth.authenticationProvider(authenticationProvider);
    }
}

Form certification

SecurityConfig.java



@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated();
        http.formLogin();
    }
}

/loginWhen you access, the login screen prepared by spring security is displayed.

I want to use my own login screen

Prepare the HTML for the login screen.

login.html


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <form th:action="@{/login}" method="post">
        <input type="text" name="username">
        <input type="password" name="password">
        <button type="submit">Login</button>
    </form>
</body>
</html>

SecurityConfig.java



@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("(A)") //add to
                .permitAll()         //add to
                .anyRequest()
                .authenticated()
        http.formLogin()
                .loginPage("(A)");  //add to
    }
}

I want to change the parameters for authentication

By default, you get it with the parameter names username and `` `password```.

SecurityConfig.java



@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/(A)") //① pass
                .permitAll()
                .anyRequest()
                .authenticated()
        http.formLogin()
                .loginPage("/(A)")
                .usernameParameter("(B)")  //add to
                .passwordParameter("(C)"); //add to
    }
}

login.html



<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <form th:action="@{/login}" method="post">
        <input type="text" name="(B)">
        <input type="password" name="(C)">
        <button type="submit">Login</button>
    </form>
</body>
</html>

I want to change the screen that transitions when authentication is successful

By default, it transitions to "/".

SecurityConfig.java



@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated()
        http.formLogin()
                .defaultSuccessUrl("/home"); // 「/Transition to "home"
    }
}

I want to authenticate with 3 or more parameters

Normally, you are authenticated with two parameters, username and `` `password```. There are several methods, but here is the method for Form authentication.

What to create

Prepare a login screen to enter the three parameters.

login.html



<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <form th:action="@{/login}" method="post">
        <input type="text" name="(A)">
        <input type="text" name="username">
        <input type="password" name="password">
        <button type="submit">Login</button>
    </form>
</body>
</html>

Create a class that inherits UsernamePasswordAuthenticationToken that holds the credentials.

MultiParamAuthenticationToken


public class MultiParamAuthenticationToken extends UsernamePasswordAuthenticationToken {

    private static final long serialVersionUID = 1L;

    private Object tenant; //Additional parameters

    public MultiParamAuthenticationToken(Object principal, Object credentials, Object tenant) {
        super(principal, credentials, Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
        this.tenant = tenant; 
    }

    public Object getTenant() {
        return this.tenant;
    }
}

Create an authentication provider class.

MultiParamAuthenticationProvider



@Configuration
public class MultiParamAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        String tenant = null;
        if (authentication instanceof MultiParamAuthenticationToken) {
            tenant = (String) ((MultiParamAuthenticationToken) authentication).getTenant();
        }
        if (!"user".equals(username) || !"pass".equals(password) || !"multi".equals(tenant)) {
            throw new BadCredentialsException("aaa");
        }
        return new MultiParamAuthenticationToken(username, password, tenant);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return MultiParamAuthenticationToken.class.isAssignableFrom(authentication);
    }

}

Prepare a filter class to get the third parameter.

MultiParamAuthenticationFilter


public class MultiParamAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {

        String username = obtainUsername(request);
        String password = obtainPassword(request);
        String tenant = obtainTenant(request);

        MultiParamAuthenticationToken authRequest = new MultiParamAuthenticationToken(
                username, password, tenant);

        return getAuthenticationManager().authenticate(authRequest);
    }

    private String obtainTenant(HttpServletRequest request) {
        return (String) request.getParameter("tenant");
    }
}

SecurityConfig.java



@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MultiParamAuthenticationProvider authenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login")
                .permitAll()
                .anyRequest()
                .authenticated();

        //Set your own filter without using formLogin
        http.addFilter(getMultiParamAuthenticationFilter());
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
    }

    private MultiParamAuthenticationFilter getMultiParamAuthenticationFilter() throws Exception {
        MultiParamAuthenticationFilter filter = new MultiParamAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManager());
        return filter;
    }
}

OAuth authentication

In order to perform OAuth authentication, it is necessary to issue a client ID and secret at the authentication provider. Here, OAuth authentication is performed by Google.

Minimum required implementation

Add the following dependency to pom.xml.

pom.xml


<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

Set the OAuth information in application.yml.

application.yml


spring:
  security:
    oauth2:
      client:
        registration:
          google:
            clientId: <Client ID>
            clientSecret: <Client secret>

SpringSecurity.java


@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated();

        http.oauth2Login(); //add to
    }

I want to use the information at the time of authentication

LoginController.java


@Controller
public class LoginController {

    @Autowired
    private OAuth2AuthorizedClientService authorizedClientService;

    @GetMapping
    public String index(OAuth2AuthenticationToken authentication, Model model) {

        OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
                authentication.getAuthorizedClientRegistrationId(),
                authentication.getName());

        model.addAttribute("name", authorizedClient.getPrincipalName());
        model.addAttribute("accessToken", authorizedClient.getAccessToken().getTokenValue());

        return "index";
    }

oauth2authenticationtokenYou can get the authentication information with.

Next, create a screen for display.

index.html


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>home</title>
</head>
<body>
    <p th:text="${accessToken}"></p>
    <p th:text="${name}"></p>
</body>
</html>

I want to authenticate with OAuth on my own login screen

Create a login screen.

login.html


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <a href="/oauth2/authorization/google">Google authentication</a>
</body>
</html>

SecurityConfig.java


@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("(A)")
                .permitAll()
                .anyRequest()
                .authenticated();

        http.oauth2Login().loginPage("(A)"); //add to
    }

Original certification

I want to authenticate with JSON

usernamepasswordauthenticationfilterCreate a class that inherits from.

JsonAuthenticationFilter.java


public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {

        try {
            //Get parameters
            //Authentication parameter information is request.getInputStream()Since it is stored in, it is taken out using Jackson
            Map<String, String> params = new ObjectMapper().readValue(request.getInputStream(),
                    new TypeReference<Map<String, String>>() {});

            //Authentication request information generation
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    params.get("(A)"), params.get("(B)"));

            //Authentication
            return getAuthenticationManager().authenticate(authRequest);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    //Process when authentication is successful
    @Override
    protected void successfulAuthentication(HttpServletRequest req,
            HttpServletResponse res,
            FilterChain chain,
            Authentication auth) throws IOException, ServletException {
        //Save authenticated user
        //If you do not save it, it will be treated as unlogged in.
        SecurityContextHolder.getContext().setAuthentication(auth);
    }
}

The login screen will send the authentication information by Ajax communication.

login.html


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <form>
        <input type="text" id="(A)" name="(A)">
        <input type="password" id="(B)" name="(B)">
        <button type="button" id="btnLogin" onclick="login()">Login</button>
    </form>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script>
        function login() {
            //Get authentication parameters
            const data = {
                email: document.getElementById('(A)').value,
                password: document.getElementById('(B)').value
            }
            //Authentication
            axios.post('(C)', data)
                .then(res => location.href = "/home") //When authentication is successful, "/Transition to "home"
        }
    </script>
</body>

</html>

Set the Filter created above with SecuritConfig.

SecurityConfig.java


@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("(C)")
                .permitAll()
                .anyRequest()
                .authenticated();

        //Ajax communication is performed, so disable it
        http.csrf().disable();

        //Set authentication filter by Json
        http.addFilter(getJsonAuthenticationFilter());
    }

    private JsonAuthenticationFilter getJsonAuthenticationFilter() throws Exception {
        JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
        //Use default authentication method
        filter.setAuthenticationManager(authenticationManager());
        //Specify the path where the Filter will be executed and the HTTP method
        filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("(C)", "POST"));
        return filter;
    }
}

Advanced version

I want to return the JWT after authentication

Add the JWT library to pom.xml.

pom.xml (additional only)


<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>

Send the credentials in JSON and return the JWT after success.

usernamepasswordauthenticationfilterCreate a class that inherits from.

JwtAuthenticationFilter.java


public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {

        try {
            //Get parameters
            //Authentication parameter information is request.getInputStream()Since it is stored in, it is taken out using Jackson
            Map<String, String> params = new ObjectMapper().readValue(request.getInputStream(),
                    new TypeReference<Map<String, String>>() {});

            //Authentication request information generation
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    params.get("(A)"), params.get("(B)"));

            //Authentication
            return getAuthenticationManager().authenticate(authRequest);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    //Create JWT when authentication is successful and set it in response header.
    @Override
    protected void successfulAuthentication(HttpServletRequest req,
            HttpServletResponse res,
            FilterChain chain,
            Authentication auth) throws IOException, ServletException {

        Date issuedAt = new Date();
        Date notBefore = new Date(issuedAt.getTime());
        Date expiresAt = new Date(issuedAt.getTime() + TimeUnit.MINUTES.toMillis(100L));

        //JWT generation
        String token = JWT.create()
                .withIssuedAt(issuedAt)
                .withNotBefore(notBefore)
                .withExpiresAt(expiresAt)
                .withClaim("(C)", (String)auth.getPrincipal())
                .sign(Algorithm.HMAC512("secret"));

        //Set JWT in Authorization header
        res.addHeader("Authorization", "Bearer " + token);
    }
}

onceperrequestfilterCreate a class that validates jwt that inherits from.

JwtAuthenticationFilter.java


public class JwtAuthorizationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {

        String header = req.getHeader("Authorization");

        if (header == null || !header.startsWith("Bearer ")) {
            //Continues processing, but does not set the credentials in the SecurityContext, resulting in a 403 return
            chain.doFilter(req, res);
            return;
        }

        //If it is a Bearer Prefix in the Authorization header
        UsernamePasswordAuthenticationToken authentication = getAuthentication(req);

        if (Objects.isNull(authentication)) {
            chain.doFilter(req, res);
            return;
        }

        SecurityContextHolder.getContext().setAuthentication(authentication);

        chain.doFilter(req, res);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {

        String token = request.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            return null;
        }
        token = token.substring("Bearer ".length());

        if (Objects.isNull(token) || token.length() == 0) {
            return null;
        }

        JWTVerifier verifier = JWT.require(Algorithm.HMAC512("secret")).build();

        try {
            DecodedJWT jwt = verifier.verify(token);

            return new UsernamePasswordAuthenticationToken(
                    jwt.getClaim("(C)").asString(), null,
                    null);
        } catch (JWTDecodeException e) {
            return null;
        }
    }

}

SecurityConfig.java


@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login")
                .permitAll()
                .anyRequest()
                .authenticated();

        http.csrf().disable();

        http.addFilter(getJsonAuthenticationFilter());

    }

    private JsonAuthenticationFilter getJsonAuthenticationFilter() throws Exception {
        JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
        //Use default authentication method
        filter.setAuthenticationManager(authenticationManager());
        //Specify the path where the Filter will be executed and the HTTP method
        filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
        return filter;
    }
}

Recommended Posts

[Reverse lookup] Spring Security (updated from time to time)
Touch all Spring "Guides" (updated from time to time)
Memorandum Poem (updated from time to time)
SpringBoot useful site collection (updated from time to time)
[Updated from time to time] Links that are indebted
Introduction to programming for college students (updated from time to time)
[Updated from time to time] Ruby on Rails Convenient methods
[Eclipse] Summary of environment settings * Updated from time to time
Try Spring Boot from 0 to 100.
[Personal memo] Frequently used Java grammar updated from time to time
Personally recommended Intellij IDEA initial settings (updated from time to time)
Upgrade spring boot from 1.5 series to 2.0 series
Summary of results of research on object orientation [Updated from time to time]
How to convert param to hash with Rails controller (updated from time to time)
Convert from java UTC time to JST time
Story when moving from Spring Boot 1.5 to 2.1
Changes when migrating from Spring Boot 1.5 to Spring Boot 2.0
Transition from Struts2 to Spring MVC (Controller)
Changes when migrating from Spring Boot 2.0 to Spring Boot 2.2
I translated the grammar of R and Java [Updated from time to time]
Points I stumbled upon when creating an Android application [Updated from time to time]
Git commands that new engineers should look back on [updated from time to time]
Get Enum by reverse lookup from the contents
[Introduction to Spring Boot] Authentication function with Spring Security
Spring Boot --How to set session timeout time
To connect from Spring to MySQL on virtual server (unsolved)
Push delivery from Spring application to Firebase Cloud Messaging
Try to work with Keycloak using Spring Security SAML (Spring 5)
My 1880's ~ Fixed time zone from LMT to JST ~
A new employee tried to create an authentication / authorization function from scratch with Spring Security