end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

spring security for java における http header/cookie認証

login認証画面も、spring boot + spring security で実装した サンプルコードはよく見かけますが、 SSO認証部分をOpenAM等の別サービスに役割分担したい為、試してみました。

目次

参考url

今回の http header/cookie認証の為、参考にしました。というより、写経です

以下は、エラー画面表示の参考にしました

以下の 14章 チュートリアルで、まず、Springの基本?を確認しました。

dir 構成

PATH NOTE
pom.xml 通常のform認証同様、spring-boot-starter-security 使用
config/WebSecurityConfig.java 認証要のpathや、login filterを指定
action/*Action.java 通常?のspring bootの @Controller です
action/ErrorAction.java implements ErrorController で 認証や権限等のerror表示
filter/AppPreAuthenticatedFilter.java login filterです。認証情報取得以外は
spring securityに任せます
model/User.java @Entity ですが、@Proxy(lazy = false) もつけています
repository/UserRepository.java 通常の JpaRepository
service/AppUserDetails.java spring security が参照する為、model/User.java を内包
service/AppUserDetailsService.java implements AuthenticationUserDetailsService
resources/application.properties postgresへの接続設定のみ記載しています

pom.xml

余計な dependency も記載していますが、 通常のform認証同様、spring-boot-starter-security 使用がポイントです。

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.5</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>jp.end0tknr</groupId>
  <artifactId>MySpring2</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>MySpring2</name>
  <description>seasar2 to spring</description>
  <properties>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.thymeleaf.extras</groupId>
      <artifactId>thymeleaf-extras-java8time</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.thymeleaf.extras</groupId>
      <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    </dependency>

  </dependencies>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>
  
</project>

config/WebSecurityConfig.java

認証要のpathや、login filterに使用する AppPreAuthenticatedFilter を記載しています。

package jp.end0tknr.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
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.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;

import jp.end0tknr.domain.service.AppUserDetailsService;
import jp.end0tknr.filter.AppPreAuthenticatedFilter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken>
    authenticationUserDetailsService() {
        return (AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken>)
                new AppUserDetailsService();
    }

    @Bean
    public PreAuthenticatedAuthenticationProvider
                    preAuthenticatedAuthenticationProvider() {
        PreAuthenticatedAuthenticationProvider provider
            = new PreAuthenticatedAuthenticationProvider();
        provider.
            setPreAuthenticatedUserDetailsService(authenticationUserDetailsService());
        provider.
            setUserDetailsChecker(new AccountStatusUserDetailsChecker());
        return provider;
    }

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

    @Bean
    public AbstractPreAuthenticatedProcessingFilter preAuthenticatedProcessingFilter()
            throws Exception {
        AppPreAuthenticatedFilter filter = new AppPreAuthenticatedFilter();

        filter.setAuthenticationManager(authenticationManager());
        return filter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
        .antMatchers("/", "/public","/error").permitAll()
        .antMatchers("/admin").hasRole("ADMIN")
        .anyRequest().authenticated()
        .and()
        .addFilter(preAuthenticatedProcessingFilter());
    }
}

action/*Action.java

通常?のspring bootの @Controller です。 今回の認証用に特別な処理は行っていません。

action/PrivateAction.java

package jp.end0tknr.action;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class PrivateAction {
    @RequestMapping(value = "/private", method = RequestMethod.GET)
    public String index(Model model) {
        model.addAttribute("message", "Hello Springboot");
        return "private";
    }
}

templates/private.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>SPRING SECURITY PRIVATE PAGE</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>SPRING SECURITY PRIVATE PAGE</h1>
    <p>
      HELLO !!!
    </p>
  </body>
</html>

action/AdminAction.java

package jp.end0tknr.action;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class AdminAction {
    @RequestMapping(value = "/admin", method = RequestMethod.GET)
    public String index(Model model) {
        model.addAttribute("message", "Hello Springboot");
        return "admin";
    }
}

templates/admin.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>SPRING SECURITY ADMIN's PAGE</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>SPRING SECURITY ADMIN's PAGE</h1>
    <p>
      HELLO !!!
    </p>
  </body>
</html>

action/PublicAction.java

package jp.end0tknr.action;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class PublicAction {

    @RequestMapping(value = "/public", method = RequestMethod.GET)
    public String index(Model model) {
        model.addAttribute("message", "Hello Springboot");
        return "public";
    }
}

templates/public.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>SPRING SECURITY PUBLIC PAGE</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>SPRING SECURITY PUBLIC PAGE</h1>
    <p>
      HELLO !!!
    </p>
  </body>
</html>

action/ErrorAction.java

認証エラーが発生すると、/error へ redirectするようですが、 error画面の表示方法を知らなかったので、取り敢えず実装しました。

403だけでなく、404や500エラーも同じ/errorで表示されるはずです。

package jp.end0tknr.action;

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ErrorAction implements ErrorController {

    @Override
    public String getErrorPath() {
      return "/error";
    }

    @RequestMapping(value = "/error")
    public String index(Model model) {
        model.addAttribute("message", "Hello Springboot");
        return "error";
    }
}

templates/error.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>SPRING SECURITY ERROR PAGE</title>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>SPRING SECURITY ERROR PAGE</h1>
    <p>
      HELLO !!!
    </p>
  </body>
</html>

filter/AppPreAuthenticatedFilter.java

認証情報取得のみ、記載すれば、後は spring securityに任せのようです。

以下では、ユーザIDを固定文字列(aaaaやcccc等)で渡していますが、

SpringBoot for java で、http headerやcookieを取得 - end0tknr's kipple - web写経開発

を参考に、http headerやcookie から取得して下さい。

package jp.end0tknr.filter;

import javax.servlet.http.HttpServletRequest;

import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;

public class AppPreAuthenticatedFilter extends AbstractPreAuthenticatedProcessingFilter{

    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
        //リクエストからユーザーID等のユーザー情報を抽出
        //return "hoge";
        //return "aaaa";
        return "cccc";

    }

    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
        //リクエストから証明書情報を抽出。DB等にある場合もある?
        return "";
    }

}

model/User.java

通常の @Entity では、エラーとなった為、@Proxy(lazy = false) を追加しています。

package jp.end0tknr.domain.model;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.annotations.Proxy;

import lombok.Data;

@Entity
@Table(name = "usr")
@Proxy(lazy = false)
@Data
public class User {
    @Id
    private String userId;
    private String password;
    private String firstName;
    private String lastName;
    private String roleName;
}
myspring=> \d usr
                          Table "public.usr"
   Column   |          Type          | Collation | Nullable | Default 
------------+------------------------+-----------+----------+---------
 user_id    | character varying(255) |           | not null | 
 first_name | character varying(255) |           | not null | 
 last_name  | character varying(255) |           | not null | 
 password   | character varying(255) |           | not null | 
 role_name  | character varying(255) |           | not null | 
Indexes:
    "usr_pkey" PRIMARY KEY, btree (user_id)
Referenced by:
    TABLE "reservation" CONSTRAINT "fk_recqnfjcp370rygd9hjjxjtg" FOREIGN KEY (user_id) REFERENCES usr(user_id)

myspring=> select * from usr;
   user_id   | first_name | last_name |                 password             | role_name 
-------------+------------+-----------+--------------------------------------+-----------
 taro-yamada | 太郎       | 山田      | $2a$10$oxSJl.keBwxms<ないしょ>UTqfTK | USER
 aaaa        | Aaa        | Aaa       | $2a$10$oxSJl.keBwxms<ないしょ>UTqfTK | USER
 bbbb        | Bbb        | Bbb       | $2a$10$oxSJl.keBwxms<ないしょ>UTqfTK | USER
 cccc        | Ccc        | Ccc       | $2a$10$oxSJl.keBwxms<ないしょ>UTqfTK | ADMIN
(4 rows)

repository/UserRepository.java

package jp.end0tknr.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import jp.end0tknr.domain.model.User;

public interface UserRepository extends JpaRepository<User, String> {
}

service/AppUserDetails.java

spring security が参照する為、model/User.java を内包し、 spring security の UserDetails が必要とするmethodを追加しています。

package jp.end0tknr.domain.service;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;

import jp.end0tknr.domain.model.User;


// spring security 認証で使用する user定義。
// jp.end0tknr.domain.model.User を内包。
// Spring徹底入門 14章も参照。
//   https://www.shoeisha.co.jp/book/detail/9784798142470
public class AppUserDetails implements UserDetails {
    private final User user;

    public AppUserDetails(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // spring securityでは「ROLE_ADMIN」のように「ROLE_」の接頭語が必要な為
        // https://qiita.com/yokobonbon/items/7d729bd8085f3fb898bb
        // https://docs.spring.io/spring-security/site/docs/current/reference/html5
        return AuthorityUtils.createAuthorityList("ROLE_" + this.user.getRoleName());
    }

    @Override
    public String getPassword() {
        return this.user.getPassword();
    }

    @Override
    public String getUsername() {
        return this.user.getUserId();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

service/AppUserDetailsService.java

implements AuthenticationUserDetailsService なserviceクラスです。

package jp.end0tknr.domain.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.stereotype.Service;

import jp.end0tknr.domain.model.User;
import jp.end0tknr.domain.repository.UserRepository;

@Service
public class AppUserDetailsService
implements  AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token)
            throws UsernameNotFoundException {

        String userName=(String)token.getPrincipal();

        // Object credentials = token.getCredentials();


        User user;
        try {
            user = userRepository.getOne(userName);
        } catch (Exception e) {
            System.out.println("NOT FOUND USER A");
            throw new UsernameNotFoundException(userName + " is not found.");
        }

        if (user == null) {
            System.out.println("NOT FOUND USER B");
            throw new UsernameNotFoundException(userName + " is not found.");
        }

        System.out.println("FOUND USER");

        return new AppUserDetails(user);
    }

}

resources/application.properties

spring.jpa.database=POSTGRESQL
spring.datasource.url=jdbc:postgresql://192.168.63.3:5432/myspring
spring.datasource.username=ないしょ
spring.datasource.password=ないしょ