[From Hello To QueryDSL] Spring Boot Security (9/12)

By | 2020년 3월 15일
Table of Contents

Spring Boot Security

개발환경

  • Spring Boot 2.1.x
  • Gradle 4.10.2

build.gradle 수정

build.gradle

......
dependencies {
    // ......
    //compile('org.springframework.boot:spring-boot-starter-security')
    //testCompile("org.springframework.security:spring-security-test")
    implementation 'org.springframework.boot:spring-boot-starter-security'
    testImplementation "org.springframework.security:spring-security-test"
    // ......
}
......

http://localhost:8080/ 에 접속하면 아이디와 비밀번호를 입력하라고 합니다.

디폴트 아이디는 user 이고, 디폴트 비밀번호는 로그에 아래와 같이 표시됩니다.

2020-03-21 15:47:05.526  INFO 14884 --- [           main] .s.s.UserDetailsServiceAutoConfiguration :

Using generated security password: aa921732-e591-4632-b583-a1547910862c

2020-03-21 15:47:05.698  INFO 14884 --- [           main] o.s.s.web.DefaultSecurityFilterChain     ......

커스텀 로그인

커스텀 아이디/패스워드 데이타베이스를 생성하고, 등록된 아이디로 로그인하도록 수정합니다.

테이블 생성

DROP TABLE `user`;

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `email` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  `picture` varchar(255) DEFAULT NULL,
  `role` varchar(255) NOT NULL,
  `passwd` varchar(255) NULL,
  `encpasswd` varchar(255) NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user`(email,name, role, passwd)
VALUES('test@gmail.com', '테스트', 'USER', '{noop}aa921732-e591-4632-b583-a1547910862c');

user 엔터티 생성

src/main/java/kr/co/episode/example/domain/user/Role.java

@Getter
@RequiredArgsConstructor
public enum Role {

    GUEST("ROLE_GUEST", "손님"),
    USER("ROLE_USER", "일반 사용자"),
    ADMIN("ROLE_ADMIN", "관리자");

    private final String key;
    private final String title;
}

src/main/java/kr/co/episode/example/domain/user/User.java

@Getter
@NoArgsConstructor
@Entity
public class User {

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

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String email;

    @Column
    private String picture;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    @Column
    private String passwd;

    @Builder
    public User(String name, String email, String picture, Role role, String passwd) {
        this.name = name;
        this.email = email;
        this.picture = picture;
        this.role = role;
        this.passwd = passwd;
    }

    public User update(String name, String picture) {
        this.name = name;
        this.picture = picture;

        return this;
    }

    public String getRoleKey() {
        return this.role.getKey();
    }
}

src/main/java/kr/co/episode/example/domain/user/UserRepository.java

public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByEmail(String email);
}

user 서비스 생성

src/main/java/kr/co/episode/example/service/user/UserService.java

@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Optional<User> userWrapper = userRepository.findByEmail(email);

        if(userWrapper.isEmpty()) {
            throw new UsernameNotFoundException("Username not found");
        }

        User user = userWrapper.get();

        List<GrantedAuthority> authorities = new ArrayList<>();

        if (("admin@example.com").equals(email)) {
            authorities.add(new SimpleGrantedAuthority(Role.ADMIN.getKey()));
        } else {
            authorities.add(new SimpleGrantedAuthority(Role.USER.getKey()));
        }

        return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPasswd(), authorities);
    }
}

src/main/java/kr/co/episode/example/config/SecurityConfig.java

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserService userService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable()
                .headers().frameOptions().disable()
                .and()
                    .authorizeRequests()
                    .antMatchers("/", "/css/**", "/images/**", "/js/**", "/login", "/profile").permitAll()
                    .antMatchers("/api/v1/**", "/posts/save").hasRole(Role.USER.name())
                    .anyRequest().authenticated()
                .and()
                    .logout()
                    .logoutSuccessUrl("/")
                .and()
                    .formLogin();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }
}

위 설정으로 데이타베이스에 입력한 계정정보를 이용해 로그인할 수 있습니다.

세션정보 이용하기

src/main/java/kr/co/episode/example/config/dto/SessionUserDto.java

@Getter
public class SessionUserDto implements Serializable {

    private String name;
    private String email;
    private String picture;

    public SessionUserDto(User user) {
        this.name = user.getName();
        this.email = user.getEmail();
        this.picture = user.getPicture();
    }
}

src/main/java/kr/co/episode/example/service/user/UserService.java

@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {

    private final UserRepository userRepository;
    private final HttpSession httpSession;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        // ......

        List<GrantedAuthority> authorities = new ArrayList<>();
        httpSession.setAttribute("user", new SessionUserDto(user));

        // ......

        return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPasswd(), authorities);
    }
}

src/main/java/kr/co/episode/example/web/IndexController.java

@RequiredArgsConstructor
@Controller
public class IndexController {

    // ......
    private final HttpSession httpSession;

    @GetMapping("/")
    public String index(Model model, @PageableDefault Pageable pageable, @RequestParam Map<String, String> params) {
        // ......

        SessionUserDto sessionUserDto = (SessionUserDto) httpSession.getAttribute("user");
        if (sessionUserDto != null) {
            model.addAttribute("userName", sessionUserDto.getName());
        }

        model.addAttribute("posts", postsPagingService.findAllDesc(pageable, postsSearchDto));
        model.addAttribute("search", postsSearchDto);

        return "index";
    }

    // ......
}

src/main/resources/templates/index.html

<div class="col-md-12">
    <div class="row">
        <div class="col-md-6">
            <a href="/posts/save" role="button" class="btn" btn-primary>글 등록</a>
            <div th:if="${userName != null}" th:inline="text">
                [[${userName}]] 님, 안녕하세요.
                <a href="/logout" class="btn btn-info active" role="button">로그아웃</a>
            </div>
            <div th:if="${userName == null}">
                <a href="/login" class="btn btn-success active" role="button">로그인</a>
            </div>
        </div>
    </div>
</div>

세션정보 어노테이션으로 가져오기

src/main/java/kr/co/episode/example/config/LoginUser.java

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}

src/main/java/kr/co/episode/example/config/LoginUserArgumentResolver.java

@RequiredArgsConstructor
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {

    private final HttpSession httpSession;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        boolean isLoginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null;
        boolean isUserClass = SessionUserDto.class.equals(parameter.getParameterType());

        return isLoginUserAnnotation && isUserClass;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return httpSession.getAttribute("user");
    }
}

src/main/java/kr/co/episode/example/config/WebConfig.java

@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final LoginUserArgumentResolver loginUserArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(loginUserArgumentResolver);
    }
}

src/main/java/kr/co/episode/example/web/IndexController.java

@RequiredArgsConstructor
@Controller
public class IndexController {

    // ......

    @GetMapping("/")
    public String index(Model model, @PageableDefault Pageable pageable, @LoginUser SessionUserDto user, @RequestParam Map<String, String> params) {
        // ......

        if (user != null) {
            model.addAttribute("userName", user.getName());
        }

        model.addAttribute("posts", postsPagingService.search(pageable, postsSearchDto));
        model.addAttribute("search", postsSearchDto);

        return "index";
    }

    // ......
}

One thought on “[From Hello To QueryDSL] Spring Boot Security (9/12)

  1. Pingback: Spring Boot Oauth2 Client – 상구리의 기술 블로그

답글 남기기