원본 본문으로 이동하기

Spring boot Security : 2. 인증 로직을 만들어보자.

박용서 - Spring boot Security 시리즈 1. 설치 및 페이지 설정 - https://gs.saro.me/#!m=elec&jn=790 2. 인증로직을 만들어보자. - https://gs.saro.me/#!m=elec&jn=791 3. 인증로직 - 잠재적 위험 - https://gs.saro.me/#!m=elec&jn=792 4. 인증 페이지뷰 - https://gs.saro.me/#!m=elec&jn=793 5. 회원가입 - https://gs.saro.me/#!m=elec&jn=794 부록 : Spring Security login (성공 / 실패) 이벤트 리스너 - https://gs.saro.me/#!m=elec&jn=825 알림 여기서부터 예제는 hibernate - PostgreSQL 기반으로 작성됩니다. 다른 부분만 별도로 작성해주시면 다른 플랫폼에서도 그대로 할 수 있습니다. DB 설정 사실 role 은 저런식으로 설정하면 안됩니다.!! 다른 테이블을 만들어서 조인하는 식으로해서 여러개의 role을 가지고 있을 수 있게 만들어야하지만 여기서는 예제가 너무 길어지는걸 방지하기 위해 이렇게 쓰도록하겠습니다. CREATE TABLE users ( no bigint NOT NULL, ac character varying(32) NOT NULL, pw character varying(256) NOT NULL, -- 원래 별도의 테이블에 만들어서 1:n 조인을 해야한다. -- 예제가 너무 길어지는걸 방지하기위해 편의상 이렇게 작성. role character varying(32) NOT NULL ); CREATE SEQUENCE users_no_seq; -- 엄밀히 설정하려면 대문자나 소문자로 변환해서 유니크를 걸어야하는데 여기서는 생략합니다. -- 사실 함수 인덱스를 거는거보다 입력시에 무조건 소문자로 치환해주는게 더 바람직. CREATE INDEX account_ac_uni ON account (ac); -- 왜 암호를 EN-1234 라고 정했는지는 있다가 설명. INSERT INTO account (ac, pw, role) VALUES ('test', 'EN-1234', 'NORMAL') 엔티티 @Entity(name="account") @Table(name="account") @Data @Getter @Setter @ToString public class Account { @Id @SequenceGenerator(name="seq", sequenceName="account_no_seq", allocationSize=1) @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq") long no; @Column(unique=true) @NotNull String ac; public void setAc(String ac) { // 일반적으로 소문자 : pg-sql 같은경우 대소문자를 구분함. // 구분하지 않더라도 소문자 입력이 일반적이다. this.ac = ac.toLowerCase(); } @Column @NotNull String pw; @Column @NotNull String role; } 리포지토리 / 서비스 아이디로 계정을 찾기위해 findOneByAc 추가함. public interface AccountRepository extends JpaRepository<Account, Long> { @Query("SELECT a FROM account a WHERE a.ac = :ac") public Account findOneByAc(@Param("ac") String ac); } @Component public class AccountService { @Autowired AccountRepository accountRepository; public Account getAccount(String ac) { return accountRepository.findOneByAc(ac); } } LoginUserDetails 로그인 유저의 객체입니다. import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; public class LoginUserDetails extends User { private static final long serialVersionUID = 1L; @Getter private long no; public LoginUserDetails(Account account) { // 일반적으로는 AuthorityUtils.createAuthorityList 에 다수의 룰을 넣고 // account 테이블과 분리되어 별도의 권한테이블을 join해서 가져와야하지만 // 예제가 길어지는걸 방지하기위해 이렇게 만들었다. super ( account.getAc(), account.getPw(), AuthorityUtils.createAuthorityList(account.getRole()) ); no = account.getNo(); } } LoginUserDetailsService - 인증 과정으로 유저가 입력한 id 에 해당하는 정보를 불러와 LoginUserDetails 에 바인딩하여 반환합니다. @Service public class LoginUserDetailsService implements UserDetailsService { @Autowired AccountService accountService; // 필자의 경우 기존 해시 시스템이 좀 다르기 때문에 // 이부분을 적용한다면 암호가 구형시스템이라면 바꿔주는 부분이 필요. // @Autowired // private HttpServletRequest request; @Override public UserDetails loadUserByUsername(String ac) throws UsernameNotFoundException { // 기존해시와 신규해시가 다를경우 이런식으로 받아 처리할 수 있음. // 위 @Autowired HttpServletRequest request; // request 처리 Account account = accountService.getAccount(ac); if (account == null) { // 계정이 존재하지 않음 throw new UsernameNotFoundException("login fail"); } return new LoginUserDetails(account); } } SecurityConfig 이전장에서 설정했던 객체입니다. 로그인 과정과 패스워드 인코더를 추가하였습니다. @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) throws Exception { // 메인페이지 : css나 js 같은것들도 여기에 포함시켜준다. web.ignoring().antMatchers("/"); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() // 위 ignoring 을 제외한 전체가 기본 인증페이지 .antMatchers("/**").authenticated(); http .formLogin() // 로그인 페이지 : 컨트롤러 매핑을 하지 않으면 기본 제공되는 로그인 페이지가 뜬다. .loginProcessingUrl("/login"); http .logout() // /logout 을 호출할 경우 로그아웃 .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // 로그아웃이 성공했을 경우 이동할 페이지 .logoutSuccessUrl("/"); } @Configuration public static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter { @Autowired UserDetailsService userDetailsService; @Bean PasswordEncoder passwordEncoder() { // 스프링에서 제공하는 기본 암호 인코더 // return new BCryptPasswordEncoder(); // 커스텀 인코더를 사용하고있다. return new MyPasswordEncoder(); } @Override public void init(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } } // 암호 인코더 커스텀 설정 public static class MyPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { // 여기서는 이렇게 처리하였지만 예를들어 sha-2 / sha-3 같은 해시를 접목시킬 수 있다. // 여기서는 간단히 EN-을 붙여 확인하는 용도! return "EN-" + rawPassword.toString(); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { // rawPassword 현재 들어온 값 | encodedPassword 매칭되는 계정에 있는 값 return encodedPassword.equals(encode(rawPassword)); } } } 컨트롤러 @ResponseBody @RequestMapping(path="/", produces="text/html") public String index() { return "index page<br/><a href=\"/mypage\">mypage</a>"; } @ResponseBody @RequestMapping(path="/mypage", produces="text/html") public String mypage(@AuthenticationPrincipal LoginUserDetails userDetails) { return "is mypage !!<br/>" + userDetails.getNo() + " : " + userDetails.getUsername() + "<br/><a href=\"/logout\">logout</a>"; } 실행 인덱스 페이지에선 인덱스 화면이 보이고, /mypage 로 이동할경우 자동으로 /login 으로 이동(SecurityConfig 참고)합니다. 현재 이 예제에선 login을 만들어주지 않았기 때문에 스프링에서 제공하는 기본 로그인이 나옵니다. 로그인이 완료되었다면 /mypage 로 들어갔을때 인증된 상태로 동작하며, /logout 으로 이동하면 로그아웃된 후 인덱스 페이지로 이동합니다. 다만, 암호 인코더 커스텀 설정은 주석대로 sha-2 이상의 강력한 해시를 사용하거나 스프링에서 기본적으로 제공하는 BCryptPasswordEncoder 를 써야합니다. - 스프링 자바