반응형
인증은 어디서나 중요한 부분인데 spring boot에서는 기본적으로 제공해주는 security가 있습니다.
설정하는 방법과 사용하는 방법이 방대하여 기본적인 설정방법만 작성합니다.
관련 정보는 아래 사이트에서 확인할 수 있습니다.
docs.spring.io/spring-security/site/docs/5.0.19.RELEASE/reference/htmlsingle/
spring.io/guides/gs/securing-web/
1. 의존성 추가
로그인 화면을 만들기 위해 "thymeleaf"도 포함합니다.
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
implementation 'org.springframework.security:spring-security-test'
2. security config 설정 및 소스
패키지 구성은 아래 사진과 같습니다.
db.changelog-2.0.xml도 추가 된것을 확인할 수 있습니다.
2-1. MvcConfig.java
화면과 연결하기 위한 web에 대한 기본적인 설정을 해줍니다.
package com.otrodevym.spring.base.common.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/static/", "classpath:/public/", "classpath:/",
"classpath:/resources/", "classpath:/META-INF/resources/", "classpath:/META-INF/resources/webjars/" };
public void addViewControllers(ViewControllerRegistry registry) {
// 경로에 해당하는 url을 forword 합니다.
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/secu/user/login").setViewName("login");
// 우선순위를 가장 높게 잡는다.
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 자원의 경로를 허용합니다.
registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
}
}
2-2. WebSecurityConfig.java
security에 대한 설정을 합니다.
package com.otrodevym.spring.base.common.security.config;
import com.otrodevym.spring.base.common.security.SecurityMemberService;
import com.querydsl.core.annotations.Config;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
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.builders.WebSecurity;
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 org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Config
@EnableWebSecurity
@AllArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 비밀번호 암호화를 위한 서비스 호출
private SecurityMemberService securityMemverService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
// 암호화
return new BCryptPasswordEncoder();
}
// 정적 자원에 대해서는 Security 설정을 적용하지 않음.
// WebSecurity는 FilterChainProxy 생성 필터입니다.
@Override
public void configure(WebSecurity web) {
// 해당 경로의 파일들은 spring security가 무시하도록합니다.
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// .csrf().disable() // csrf를 사용할지 여부
.authorizeRequests() // HttpServletRequest에 따라접근을 제한
.antMatchers("/admin/**").hasRole("ADMIN") // url에 따른 권한만 허용
.antMatchers("/info").hasRole("MEMBER") // url에 따른 권한만 허용
.antMatchers("/**").permitAll() // url에 따른 모두 허용
.and()
.formLogin() // form 기반 로그인 인증 관련하며 HttpSession 이용
.loginPage("/login") // 지정하고 싶은 로그인 페이지 url
.usernameParameter("email") // 지정하고 싶은 username name 명칭이며, 기본은 username
.passwordParameter("upwd") // 지정하고 싶은 password name 명칭이며, 기본은 password
.defaultSuccessUrl("/login/result") // 로그인 성공 시 이동페이지
.permitAll() // 모두 접근 허용
.and()
.csrf() // csrf 사용
.ignoringAntMatchers("/h2-console/**") // csrf 제외 url
.ignoringAntMatchers("/post/**") // csrf 제외 url
.ignoringAntMatchers("/admin/**") // csrf 제외 url
.and()
.logout() // 로그아웃
.logoutRequestMatcher(new AntPathRequestMatcher(("/logout"))) // 지정하고 싶은 로그아웃 url
.logoutSuccessUrl("/index") // 성공 시 이동 페이지
.invalidateHttpSession(true) // 세션 초기화
// .deleteCookies("cookies") // 특정 쿠키를 제거
.and()
.exceptionHandling() // 에러 처리
.accessDeniedPage("/error"); // 에러 시 이동할 페이지
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 비밀번호 암호화
auth.userDetailsService(securityMemverService).passwordEncoder(bCryptPasswordEncoder());
}
}
2-3. application.yml 추가 설정
thymeleaf에 대한 설정을 합니다.
thymeleaf:
check-template-location: true
suffix: .html
mode: HTML5
characterEncoding: utf-8
cache: false
order: 0
prefix: classpath:/templates/
3. spring security 테스트 소스
테스트 하기 위한 예제입니다.
3-1. SecurityController.java
package com.otrodevym.spring.base.common.security;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@AllArgsConstructor
@Log4j2
public class SecurityController {
private SecurityMemberService securityMemberService;
// 메인 페이지
@GetMapping("/index")
public String index() { log.info("index"); return "index"; }
// 회원 가입 페이지
@GetMapping("/signup")
public String dispSignup() { log.info("dispSignup : /signup"); return "signup"; }
// 회원가입 처리
@PostMapping("/signup")
public String execSignup(SecurityMemberDTO securityMemberDTO) { log.info("execSignup : /signup");
securityMemberService.siginupMember(securityMemberDTO); return "redirect:/secu/user/login"; }
// 로그인 페이지
@GetMapping("/login")
public String dispLogin() { log.info("dispLogin"); return "login"; }
// 로그인 결과 페이지
@GetMapping("/login/result")
public String dispLoginResult() { log.info("dispLoginResult"); return "login_success"; }
// 로그아웃 페이지
@GetMapping("/logout/result")
public String dispLogoutResult() { log.info("dispLogoutResult"); return "logout"; }
@GetMapping("/info")
public String dispInfo() { log.info("dispInfo"); return "info"; }
@GetMapping("/admin")
public String dispAdmin() { log.info("dispAdmin"); return "admin"; }
}
3-2. SecurityMemberService.java
package com.otrodevym.spring.base.common.security;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
@AllArgsConstructor
@Slf4j
public class SecurityMemberService implements UserDetailsService {
private SecurityRepository securityRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<SecurityMemberEntiry> securityMemberEntiryOptional = securityRepository.findByEmail(username);
SecurityMemberEntiry securityMemberEntiry = securityMemberEntiryOptional.orElse(null);
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(securityMemberEntiry.getRoleKey()));
// return new SecurityCustomDTO(securityMemberEntiry, securityMemberEntiry.getName(),
// securityMemberEntiry.getUpwd(), authorities);
if ("admin@test.com".equals(username)) {
authorities.add(new SimpleGrantedAuthority(Role.ADMIN.getKey()));
} else {
authorities.add(new SimpleGrantedAuthority(Role.MEMBER.getKey()));
}
log.info("username : " + username);
return new User(securityMemberEntiry.getEmail(), securityMemberEntiry.getUpwd(), authorities);
}
public SecurityMemberEntiry siginupMember(SecurityMemberDTO securityMemberVO) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
securityMemberVO.setUpwd(bCryptPasswordEncoder.encode(securityMemberVO.getUpwd()));
return securityRepository.save(securityMemberVO.toEntity());
}
}
3-3. SecurityMemberEntiry.java
package com.otrodevym.spring.base.common.security;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@Entity(name = "member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SecurityMemberEntiry extends BaseTimeEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 50, unique = true, nullable = false)
private String email;
@Column(unique = true, nullable = false)
private String name;
@Column(unique = true, length = 300)
private String upwd;
@Enumerated(EnumType.STRING)
@Column
private Role role;
@Builder
public SecurityMemberEntiry(Long id, String email, String name, String upwd, Role role) {
this.id = id;
this.email = email;
this.name = name;
this.upwd = upwd;
this.role = role;
}
public String getRoleKey() {
return this.role.getKey();
}
public SecurityMemberEntiry update(String name, String email) {
this.name = name;
this.email = email;
return this;
}
}
3-4. SecurityMemberDTO.java
package com.otrodevym.spring.base.common.security;
import lombok.*;
import java.time.LocalDateTime;
@Getter
@Setter
@ToString
@NoArgsConstructor
public class SecurityMemberDTO {
private Long id;
private String name;
private String email;
private String upwd;
private Role role;
private LocalDateTime createdDate;
private LocalDateTime modifiedDate;
public SecurityMemberEntiry toEntity() {
return SecurityMemberEntiry.builder()
.id(this.id)
.name(this.name)
.email(this.email)
.upwd(this.upwd)
.role(Role.MEMBER)
.build();
}
@Builder
public SecurityMemberDTO(Long id, String name, String email, String upwd, Role role, LocalDateTime createdDate, LocalDateTime modifiedDate) {
this.id = id;
this.name = name;
this.email = email;
this.upwd = upwd;
this.role = role;
}
}
3-5. BaseTimeEntity.java
package com.otrodevym.spring.base.common.security;
import lombok.Getter;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {
@CreationTimestamp
@Column(updatable = false)
private LocalDateTime createdDate;
@UpdateTimestamp
@Column()
private LocalDateTime modifiedDate;
}
3-6. SecurityRepository.java
package com.otrodevym.spring.base.common.security;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SecurityRepository extends JpaRepository<SecurityMemberEntiry, Long> {
Optional<SecurityMemberEntiry> findByEmail(String username);
}
3-9. Role.java
package com.otrodevym.spring.base.common.security;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum Role {
ADMIN("ROLE_ADMIN", "어드민"),
MEMBER("ROLE_MEMBER", "사용자");
private String key;
private String title;
}
3-10. admin.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>admin</title>
</head>
<body>
<h1>어드민 페이지</h1>
<hr>
</body>
</html>
3-11. error.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Error</title>
</head>
<body>
<h1>에러 페이지</h1>
</body>
</html>
3-12. index.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title>Main</title>
</head>
<body>
<h1>메인 페이지</h1>
<hr>
<a sec:authorize="isAnonymous()" th:href="@{/login}">로그인</a>
<a sec:authorize="isAuthenticated()" th:href="@{/logout}">로그아웃</a>
<a sec:authorize="isAnonymous()" th:href="@{/signup}">회원가입</a>
<a sec:authorize="hasRole('ROLE_MEMBER')" th:href="@{/info}">내정보</a>
<a sec:authorize="hasRole('ROLE_ADMIN')" th:href="@{/admin}">어드민</a>
</body>
</html>
3-13. info.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>내 정보</title>
</head>
<body>
<h1>내 정보 페이지</h1>
</body>
</html>
3-14. login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form th:action="@{/login}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<input type="text" name="email" placeholder="이메일 입력해주세요">
<input type="password" name="upwd" placeholder="비밀번호">
<button type="submit">로그인</button>
</form>
</body>
</html>
3-15. login_success.html
<!DOCTYPE html>
<html lang="en" xmlns:sec="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>로그인 성공</title>
</head>
<body>
<h1>로그인 성공!</h1>
<p>
<span sec:authentication="name"></span> 님 안녕하세요
</p>
<a th:href="@{'/index'}">메인 페이지</a>
</body>
</html>
3-16. logout.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>로그아웃</title>
</head>
<body>
<h1>로그아웃 완료!</h1>
<hr>
<a th:href="@{'/index'}">메인 페이지</a>
</body>
</html>
3-17. signup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form th:action="@{/signup}" method="post">
<input type="text" name="email" placeholder="이메일 입력해주세요">
<br>
<input type="text" name="name" id="name" placeholder="Your ID"/>
<br>
<input type="password" name="upwd" placeholder="비밀번호">
<br>
<input type="password" name="re_upwd" id="re_pass" placeholder="Repeat your password"/>
<br>
<button type="submit">가입하기</button>
</form>
</body>
</html>
3-18. db.changelog-2.0.xml
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
<changeSet author="otrodevym" id="changelog-2.0">
<createTable tableName="member">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true"/>
</column>
<column name="email" type="varchar(30)"></column>
<column name="name" type="varchar(30)"></column>
<column name="role" type="varchar(30)"></column>
<column name="upwd" type="varchar(300)"></column>
<column name="modified_date" type="timestamp" defaultValueDate="now()">
<constraints nullable="false"/>
</column>
<column name="created_date" type="timestamp" defaultValueDate="now()">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
4. 실행 결과
index로 접속합니다
메인 페이지와 회원가입
로그인 화면, 로그인 성공 결과, 메인페이지 변화, 내정보 페이지
반응형
'개발(합니다) > Java&Spring' 카테고리의 다른 글
[spring boot 설정하기-10] dev-tools 설정 및 테스트 소스 (0) | 2021.04.28 |
---|---|
[spring boot 설정하기-9] oauth2 설정 및 테스트 소스 (1) | 2021.04.23 |
[spring boot 설정하기-7] restdocs 설정 및 테스트 소스 (0) | 2021.04.10 |
[spring boot 설정하기-6] querydsl(+JPA) 설정 및 테스트 소스 (0) | 2021.04.09 |
[spring boot 설정하기-5] liquibase 설정 및 연동 (0) | 2021.04.08 |