단의 개발 블로그

Security 본문

Web/Spring

Security

danso 2024. 10. 8. 23:14

Security란?

웹 애플리케이션에서 정보는 귀중한 자산이다. 해커들은 안전하지 않은 애플리케이션에서 정보를 훔쳐간다. 개발자는 애플리케이션에 있는 정보를 보호하는 조치를 취해야 한다. 특히 사용자의 아이디, 패스워드, 계좌 정보 등은 대부분의 서비스에 가장 중요하다. 스프링 애플리케이션은 이러한 보안 설정을 쉽게 사용할 수 있는 기능이 구현되어 있다. 이것을 스프링 시큐리티라고 하는데, gradle에 해당 의존성을 명세에 추가한다.

implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

해당 설정을 저장한 후, 애플리케이션을 실행하면 서버 로그에 패스워드 로그가 출력되고, http 요청을 하면 로그인 화면이 나온다.

 

사용자 이름에 user, 로그에 출력된 패스워드를 입력하면 다음 화면으로 이동할 수 있다. 이처럼 우리 애플리케이션에 유저를 확인하고 해당 유저가 맞으면 애플리케이션으로 이동해주는 역할을 시큐리티가 진행한다. 해당 기능으로 소셜로그인, 관리자 페이지 접속, 악성 요청 차단 등의 수행을 한다. 보통 이러한 과정을 인증/인가라고 부른다. 

 

시큐리티 설정하기

시큐리티 설정을 추가할 경우 아래 보안 구성이 제공된다.

  • 모든 HTTP 요청은 인증되어야 한다.
  • 어떤 특정 역할이나 권한이 없다.
  • 로그인 페이지가 없다.
  • 시큐리티를 사용하여 HTTP 기본 인증을 사용해서 인증된다.
  • 사용자는 한개만 존재하며 user, 패스워드는 암호화 된다.

하지만 서비스를 운영하면 다양한 환경을 마주하게 된다. 이러한 보안 기능은 우리 서비스에서 필요로 하는 보안이 아니다. 그래서 스프링 시큐리티를 설정하여 우리 서비스에 맞게 변경해서 사용해야 한다. 따라서 우리는 적합한 사용자 스토어를 아래 요구사항에 맞게 구성해야 한다.

  • 스프링 시큐리티의 HTTP 인증 상자 대신 우리의 로그인 페이지로 인증한다.
  • 다수의 사용자를 제공하며, 새로운 고객이 사용자로 등록되는 회원가입을 할 수 있어야 한다.
  • 서로 다른 HTTP 요청 마다 서로 다른 보안 규칙을 적용한다.

먼저 config/SecurityConfig.java 파일을 생성하고 아래 내용을 입력한다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(au ->
        {
          au.requestMatchers("/design", "/orders").hasRole("USER");
          au.requestMatchers("/", "/**").permitAll();
        }).httpBasic(withDefaults());
    return http.build();
  }
}
  • 최신 버전의 스프링 시큐리티는 더이상 WebSecurityConfigurerAdapter를 상속받아 사용하지 않고, Bean으로 등록해서 사용한다. 또한 필터 체인 방식을 나열하지 않고, 람다를 사용하여 규칙을 정의한다.
  • 파라미터로 전달 받는 http에 체이닝 방식으로 함수를 나열하고 그 속에 람다를 사용한다.
  • authorizeHttpRequests는 http요청경로와 해당 경로의 권한을 확인하는 역할을 한다.
  • httpBasic은 base64로 인코딩 된 유저의 아이디와 패스워드를 전달하는 방식으로 사용한다. 해당 메소드는 https 에서만 사용해야 하는데, 실습을 위해 잠시 해당 메소드를 추가한다.

해당 설정을 하고 프로젝트를 실행하면 이젠 루트 경로를 요청해도 로그인 박스가 뜨지 않는다. 유저를 추가하려면 아래 코드를 Security.java에 추가한다.

...
@Bean
public UserDetailsService users() {
	UserBuilder users = User.withDefaultPasswordEncoder();
	UserDetails user = users
		.username("user")
		.password("password")
		.roles("USER")
		.build();
	UserDetails admin = users
		.username("admin")
		.password("password")
		.roles("USER", "ADMIN")
		.build();
	return new InMemoryUserDetailsManager(user, admin);
}
...
  • 스프링 시큐리티에 사용자와 관리자를 추가한다.
  • 먼저 사용자는 기본 패스워드 인코딩 방식을 사용한다. 해당 방식을 먼저 지정하면 비밀번호 암호화 관련 오류가 발생하지 않는다. 실제 서비스에선 이렇게 하면 안된다!!
  • 유저명은 user, admin으로 생성하고 비밀번호는 password로 설정한다.
  • 해당 권한은 각각 USER, ADMIN으로 하고 각각 만든다.
  • 여기선 임시로 인메모리 사용자 스토어를 사용했다.

해당 설정을 추가하고 서비스를 다시 실행하면 우리가 만든 사용자로 접속할 수 있다. 우리가 생각하는 요구사항에 맞게 변경하기 전에 먼저 사용자 스토어 종류가 무엇이 있는지 알아야 서비스에 맞게 설정할 수 있다. 시큐리티에서 제공하는 사용자 스토어는 다음과 같다.

  • 인메모리 사용자 스토어
  • JDBC 기반 사용자 스토어
  • LDAP 기반 사용자 스토어
  • 커스텀 사용자 명세 서비스

 

In Memory Authentication

인메모리 사용자 스토어는 메모리에 저장된 사용자 인증 기반을 지원한다. 해당 내용은 앞서 살펴본 것과 같이, UserDetailsService를 빈으로 등록하고 InMemoryUserDetailsManager로 생성한 유저를 반환하면 된다. 따라서 해당 내용은 코드 없이 넘어간다!

 

JDBC Authentication

JDBC 사용자 스토어는 데이터 베이스에서 유지, 관리 하는 방식이다. 해당 방식도 마찬가지로 UserDetailsService를 구현해서 사용하면 된다. JdbcUserDetailsManager는 JdbcDaoImpl를 확장하여 UserDetailsManager 인터페이스를 통해 UserDetails의 관리 기능을 제공한다. 해당 내용은 스프링 공식문서에 나와 있는 내용인데, 처음 스프링을 접하면 해당 내용이 어려우니 간단하게 코드로 살펴보자!

UserDetailsService

public interface UserDetailsService {
  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  • 아마 시큐리티를 사용하면 해당 메소드를 보게 될 것이다. 해당 인터페이스는 사용자 이름을 기반으로 데이터를 로드한다. 
  • 그래서 위에서 언급했던 JdbcUserDetailsManager는 JdbcDaoImpl의 기능을 확장하여 사용하고, JdbcDaoImpl은 UserDetailsManager를 구현하여 사용자를 찾는다.
 

JdbcDaoImpl

public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService, MessageSourceAware {

  public static final String DEFAULT_USER_SCHEMA_DDL_LOCATION = "org/springframework/security/core/userdetails/jdbc/users.ddl";

  // @formatter:off
  public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled "
    + "from users "
    + "where username = ?";
  // @formatter:on
  ...
	private String usersByUsernameQuery;

	private String rolePrefix = "";

	private boolean usernameBasedPrimaryKey = true;

	private boolean enableAuthorities = true;

	private boolean enableGroups;

	public JdbcDaoImpl() {
		this.usersByUsernameQuery = DEF_USERS_BY_USERNAME_QUERY;
		this.authoritiesByUsernameQuery = DEF_AUTHORITIES_BY_USERNAME_QUERY;
		this.groupAuthoritiesByUsernameQuery = DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY;
	}

...
  ...
  @Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		List<UserDetails> users = loadUsersByUsername(username);
		if (users.size() == 0) {
			this.logger.debug("Query returned no results for user '" + username + "'");
			throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.notFound",
					new Object[] { username }, "Username {0} not found"));
		}
		UserDetails user = users.get(0); // contains no GrantedAuthority[]
		Set<GrantedAuthority> dbAuthsSet = new HashSet<>();
		if (this.enableAuthorities) {
			dbAuthsSet.addAll(loadUserAuthorities(user.getUsername()));
		}
		if (this.enableGroups) {
			dbAuthsSet.addAll(loadGroupAuthorities(user.getUsername()));
		}
		List<GrantedAuthority> dbAuths = new ArrayList<>(dbAuthsSet);
		addCustomAuthorities(user.getUsername(), dbAuths);
		if (dbAuths.size() == 0) {
			this.logger.debug("User '" + username + "' has no authorities and will be treated as 'not found'");
			throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.noAuthority",
					new Object[] { username }, "User {0} has no GrantedAuthority"));
		}
  
  ...
  • 해당 클래스가 JDBC 쿼리를 사용하여 사용자 세부 정보를 검색하는 UserDetailsService의 구현체다. 
  • loadUserByUsername 메소드가 구현되어 있다.

JdbcUserDetailsManager.java의 일부분...

public class JdbcUserDetailsManager extends JdbcDaoImpl implements UserDetailsManager, GroupManager {

  public static final String DEF_CREATE_USER_SQL = "insert into users (username, password, enabled) values (?,?,?)";

  public static final String DEF_DELETE_USER_SQL = "delete from users where username = ?";

  public static final String DEF_UPDATE_USER_SQL = "update users set password = ?, enabled = ? where username = ?";

  public static final String DEF_INSERT_AUTHORITY_SQL = "insert into authorities (username, authority) values (?,?)";

  public static final String DEF_DELETE_USER_AUTHORITIES_SQL = "delete from authorities where username = ?";

  public static final String DEF_USER_EXISTS_SQL = "select username from users where username = ?";

  public static final String DEF_CHANGE_PASSWORD_SQL = "update users set password = ? where username = ?";

...
	@Override
	protected List<UserDetails> loadUsersByUsername(String username) {
		return getJdbcTemplate().query(getUsersByUsernameQuery(), this::mapToUser, username);
	}
...
  • 해당 코드를 보면 SQL문이 작성되어 있다. 뭔가 유저를 select, insert, delete, update를 하는 클래스로 보인다. 
  • 여기서도 loadUsersByUsername 메소드가 구현되어 있었다. jdbc 쿼리 인자 값으로 getUsersByUsernameQuery를 호출하는데 해당 쿼리문은 jdbcDaoImpl에 변수 값을 사용한다.

JDBC로 사용할 경우 기본 스키마는 org/springframework/security/core/userdetails/jdbc/users.ddl 여기에 작성된 테이블을 사용한다. 

users.ddl

#group
create table groups (
	id bigint generated by default as identity(start with 0) primary key,
	group_name varchar_ignorecase(50) not null
);

create table group_authorities (
	group_id bigint not null,
	authority varchar(50) not null,
	constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);

create table group_members (
	id bigint generated by default as identity(start with 0) primary key,
	username varchar(50) not null,
	group_id bigint not null,
	constraint fk_group_members_group foreign key(group_id) references groups(id)
);

#user
create table users(
	username varchar_ignorecase(50) not null primary key,
	password varchar_ignorecase(500) not null,
	enabled boolean not null
);

create table authorities (
	username varchar_ignorecase(50) not null,
	authority varchar_ignorecase(50) not null,
	constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);

#oracle
CREATE TABLE USERS (
    USERNAME NVARCHAR2(128) PRIMARY KEY,
    PASSWORD NVARCHAR2(128) NOT NULL,
    ENABLED CHAR(1) CHECK (ENABLED IN ('Y','N') ) NOT NULL
);


CREATE TABLE AUTHORITIES (
    USERNAME NVARCHAR2(128) NOT NULL,
    AUTHORITY NVARCHAR2(128) NOT NULL
);
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_UNIQUE UNIQUE (USERNAME, AUTHORITY);
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_FK1 FOREIGN KEY (USERNAME) REFERENCES USERS (USERNAME) ENABLE;

 

스프링에서 제공하는 방식 그대로 사용할 수도 있지만, 서비스가 복잡해지고 다양한 권한을 설정해야 할 경우가 발생할 수도 있다. 먼저 yaml파일에 해당 옵션을 추가한다. 해당 옵션은 프로젝트 경로에서 schema.sql과 data.sql이 있을 경우 항상 실행해서 테이블과 데이터를 입력해주는 설정이다. 따라서 한번만 실행하고 다음엔 실행되지 않도록 embeded로 변경한다.

spring:
  sql:
    init:
      mode: always

 

그리고 프로젝트 resources폴더 아래에 schema.sql, data.sql 각각 생성하고 아래와 같이 입력해서 사용하면 된다.

data.sql

insert into users(username, password) values ('user1', 'password1');
insert into users(username, password) values ('user2', 'password2');

insert into authorities (username, authority) values ('user1', 'ROLE_USER');
insert into authorities (username, authority) values ('user2', 'ROLE_USER');

commit;

 

schema.sql

drop table if exists users;
drop table if exists authorities;
drop table if exists ix_auth_username;

create table if not exists users(
    username varchar(50) not null primary key,
    password varchar(200) not null,
    enabled char(1) default '1'
);

create table if not exists authorities(
    username varchar(50) not null,
    authority varchar(50) not null,
    constraint fk_authorities_users
    foreign key (username) references users(username)
);

create unique index ix_auth_username on authorities(username, authority);

SecurityConfig 최종

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  private final DataSource dataSource;
  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(au ->
        {
          au.requestMatchers("/design", "/orders").hasRole("USER");
          au.requestMatchers("/", "/**").permitAll();
        }).httpBasic(withDefaults());
    return http.build();
  }

  @Autowired
  public void configureGlobal(AuthenticationManagerBuilder auth)
      throws Exception {
    auth.jdbcAuthentication()
        .dataSource(dataSource);
  }
}
  • DataSource를 주입 받아 사용한다. 
  • configureGlobal에서 jdbcAuthentication이 우리가 사용하는 mariaDB와 연결되도록 dataSource를 인자값으로 전달한다.
  • 서비스를 실행하고 insert한 유저를 입력하면 에러가 발생한다. 해당 에러는 비밀번호를 암호화 해서 사용하지 않았기 때문에 발생한 에러이며, insert할 때 '{noop}'을 추가로 붙여주면 발생하지 않는다. 실 서비스에선 이렇게 사용하면 큰일난다!!
  • 만약 해당 스키마와 연동되는 쿼리문을 커스터 마이징 해서 사용하려면 jdbcAuthentication 필터에 추가해서 사용한다.
  • .usersByUsernameQuery("select email,password,enabled " + "from bael_users " + "where email = ?") .authoritiesByUsernameQuery("select email,authority " + "from authorities " + "where email = ?");

 

LDAP 

LDAP은 Lightweight Directory Access Protocol의 약자로 분산 디렉토리 서비스에서 사용자, 시스템, 네트워크 등의 정보를 공유하기 위한 프로토콜인데, 한마디로 경량 디렉토리 접근 프로토콜이라고 이해하면 된다. 해당 프로토콜이 경량인 이유는 X.500이라 는 Directory Access Protocol의 경량화 된 버전이라 불리고, TCP/IP 레이에서 동작한다. ldap://xxx.xxx.xxx:port와 같은 형태로 http와 유사한 점이 많지만 http는누구나 request, response를 할 수 있지만, ldap는 인증된 유저만 request, response를 할 수 있다. 또한 평문으로 전송되는 http에 비해 ldap는 base64로 인코딩 되어 binary 포맷으로 전송하기 때문에 보안성이 좋다. ldap는 LDIF (LDAP Data Interchage Format)으로 데이터를 저장한다.

 

LDAP를 사용하기 위한 의존성을 추가한다.

implementation "org.springframework.ldap:spring-ldap-core"
implementation "org.springframework.security:spring-security-ldap"
implementation "com.unboundid:unboundid-ldapsdk"

 

Yaml 설정에 해당 값을 추가한다.

spring:
  ldap:
    embedded:
      ldif: classpath:users.ldif
      base-dn: dc=springframework,dc=org
      port: 9000

SecurityConfig의 메소드를 변경한다

@Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(au ->
        {
          au.requestMatchers("/design", "/orders").authenticated();
          au.requestMatchers("/", "/**").permitAll();
        }).httpBasic(withDefaults());
    return http.build();
  }

  @Autowired
  public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
        .ldapAuthentication()
        .userDnPatterns("uid={0},ou=people")
        .groupSearchBase("ou=groups")
        .contextSource()
        .url("ldap://localhost:9000/dc=springframework,dc=org")
        .and()
        .passwordCompare()
        .passwordEncoder(new BCryptPasswordEncoder())
        .passwordAttribute("userPassword");
  }
  • userSearchBase, groupSearchBase는 사용자, 그룹을 찾기 위한 기준점 쿼리를 제공한다.
  • userSearchFilter, groupSearchFilter는 기본 쿼리에 필터를 제공하기 위해 사용한다. 여기서는 사용하지 않닸다.
  • contextSource()메소드는 ContextBuilder를 반환한다. 해당 ldap서버가 어디에 위치하는지 url을 설정할 수 있다.
  • passwordCompare는 비밀번호를 비교함을 지정하고, passwordEncoder는 비밀번호 암호화 방식을 지정한다. passwordAttribute는 ldap서버에 있는 userPassword 속성의 값과 비교한다는 동작을 지정한다.

LDAP서버가 시작될 때는 classpath에 있는 LDIF파일로 부터 데이터를 로드한다. 해당 파일은 resources아래에 users.idif로 생성한다.

users.idif

dn: dc=springframework,dc=org
objectclass: top
objectclass: domain
objectclass: extensibleObject
dc: springframework

dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=subgroups,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: subgroups

dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people

dn: ou=space cadets,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: space cadets

dn: ou=\"quoted people\",dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: "quoted people"

dn: ou=otherpeople,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: otherpeople

dn: uid=ben,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Ben Alex
sn: Alex
uid: ben
userPassword: $2a$10$c6bSeWPhg06xB1lvmaWNNe4NROmZiSpYhlocU/98HNr2MhIOiSt36

dn: uid=bob,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Bob Hamilton
sn: Hamilton
uid: bob
userPassword: bobspassword

dn: uid=joe,ou=otherpeople,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Joe Smeth
sn: Smeth
uid: joe
userPassword: joespassword

dn: cn=mouse\, jerry,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Mouse, Jerry
sn: Mouse
uid: jerry
userPassword: jerryspassword

dn: cn=slash/guy,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: slash/guy
sn: Slash
uid: slashguy
userPassword: slashguyspassword

dn: cn=quote\"guy,ou=\"quoted people\",dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: quote\"guy
sn: Quote
uid: quoteguy
userPassword: quoteguyspassword

dn: uid=space cadet,ou=space cadets,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Space Cadet
sn: Cadet
uid: space cadet
userPassword: spacecadetspassword



dn: cn=developers,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: developers
ou: developer
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
uniqueMember: uid=bob,ou=people,dc=springframework,dc=org

dn: cn=managers,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: managers
ou: manager
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
uniqueMember: cn=mouse\, jerry,ou=people,dc=springframework,dc=org

dn: cn=submanagers,ou=subgroups,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfUniqueNames
cn: submanagers
ou: submanager
uniqueMember: uid=ben,ou=people,dc=springframework,dc=org

해당 값에서 타 유저는 비밀번호가 암호화가 되지 않아서 인증 유저로 체크가 되지 않는다. ben 유저만 benpassword로 인코딩 되어 있으므로 해당 사용자가 ben/benpassword로 로그인하면 인증된 사용자로 체크한다. ldif에서 유저 권한 설정 방법이 기존에 사용하던 방법이랑 다르기 때문에 request 요청에서 hasRole을 authenticated()로 변경했다. 정리하자면.. ben 사용자는 인증된 사용자 이기 때문에 타코 주문을 할 수 있게 된다.

 

커스텀 사용자 명세

스프링에 내장된 사용자를 사용하면 편리하고 일반적인 용도로 사용하기 좋다. 하지만 내장된 스토어가 우리 요구를 충족하지 못한다면, 명세 서비스를 생성하고 구성해야 한다. 서비스에서 사용하는 모든 정보는 데이터 JPA를 사용하는데, 사용자 인증 부분도 JPA를 사용해서 관리하면 더 편해진다. 

domain/User.java

@Entity
@Builder
@Data
@RequiredArgsConstructor
public class User implements UserDetails {

  private static final long serialVersionUID = 1L;

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  private String username;
  private String password;
  private String fullname;
  private String street;
  private String city;
  private String state;
  private String zip;
  private String phoneNumber;

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return List.of(new SimpleGrantedAuthority("ROLE_USER"));
  }

  @Override
  public boolean isAccountNonExpired() {
    return UserDetails.super.isAccountNonExpired();
  }

  @Override
  public boolean isAccountNonLocked() {
    return UserDetails.super.isAccountNonLocked();
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return UserDetails.super.isCredentialsNonExpired();
  }

  @Override
  public boolean isEnabled() {
    return UserDetails.super.isEnabled();
  }

  @Builder
  public User(String username, String password,
      String fullname, String street, String city, String state, String zip, String phoneNumber) {
    this.username = username;
    this.password = password;
    this.fullname = fullname;
    this.street = street;
    this.city = city;
    this.state = state;
    this.zip = zip;
    this.phoneNumber = phoneNumber;
  }
}

 

  • User 클래스는 스프링 시큐리티 인터페이스인 UserDetails 인터페이스를 구현한다.
  • 해당 구현체는 사용자 계정의 권한과 사용 여부를 나타낸다. getAuthorities() 외에 나머지는 이미 상위 클래스에서 기본 값으로 true를 사용하기 때문에 불러오지 않아도 문제가 없다. 해당 권한에 유저만 ROLE_USER로 리턴한다. 

repository/UserRepository.java

import com.exam.tacojava.domain.User;
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User, Long> {
  User findByUsername(String username);

}

 

service/UserRepositoryUserDetailsService.java

@Service
@RequiredArgsConstructor
public class UserRepositoryUserDetailsService implements UserDetailsService {

  private final UserRepository userRepository;
  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findByUsername(username);
    if (user != null) {
      return user;
    }
    throw new UsernameNotFoundException("exception user:"+ username);
  }
}
  • 이전에 살펴봤던 UserDetailsService를 직접 상속받아서 사용한다.
  • loadUserByUsername 메소드를 재 정의하여 사용하고, 해당 메소드는 null 대신 exception을 리턴한다.

SecurityConfig를 수정한다.

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

  private final UserDetailsService userDetailsService;

  @Bean
  public AuthenticationManager authenticationManager(
      AuthenticationConfiguration authenticationConfiguration)
      throws Exception {
    return authenticationConfiguration.getAuthenticationManager();
  }

  @Bean
  public PasswordEncoder encoder() {
    return new BCryptPasswordEncoder();
  }
  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);
    auth.userDetailsService(userDetailsService).passwordEncoder(encoder());
    http
        .authorizeHttpRequests(au ->
        {
          au.requestMatchers("/design", "/orders").hasRole("USER");
          au.requestMatchers("/", "/**").permitAll();
        })
        .httpBasic(withDefaults());
    return http.build();
  }
}

 

 

 

참고

https://docs.spring.io/spring-security/reference

https://spring.io/guides/gs/authenticating-ldap#header

https://it-techtree.tistory.com/entry/Java-Apply-Security-SpringBoot-Application-With-LDAP

https://www.aladin.co.kr/m/mproduct.aspx?ItemId=239755024&srsltid=AfmBOopBJang5a4JJMwSMTG4LpPtjgIx4iaywBhafy2xWl19AYTQr8mk

'Web > Spring' 카테고리의 다른 글

Spring 구성속성  (0) 2024.11.01
Security 사용하기  (3) 2024.10.08
JPA  (1) 2024.09.05
데이터 베이스  (0) 2024.09.04
사용자 요청 처리하기  (0) 2024.09.03