이 문서는 가리사니 개발자 포럼에 올렸던 글의 백업 파일입니다. 오래된 문서가 많아 현재 상황과 맞지 않을 수 있습니다.
Spring Security OAuth 통합 로그인 시리즈
서론 (잡담)
필자가 운영하고 있는 가리사니도 사로를 통해서 페이스북과 네이버 OAuth 처리가 되어있는 것을 확인할 수 있습니다. 다만 이 글을 쓰는 시점으로 가리사니와 사로는 스프링이 아닌 서블릿으로 작성된 사이트이며 1장에서 소개한 구조를 통해 그냥 자바로 끄적끄적 작성한 OAuth 입니다. 이번에 스프링으로 사이트를 리뉴얼하면서 OAuth를 작성하려는데.. 스프링의 OAuth Client를 처음 본 순간 ‘그냥 생자바로 짜는 것이 10배는 더 간단하겠다’ 라고 생각했습니다. 그래서 이 강의를 작성하게 되었습니다.
구조
- 사이트에는 스프링 시큐리티로 작성된 일반적인 로그인이 존재합니다.
- 스프링 시큐리티 참고 : /2016/07/24/%EB%B0%B1%EC%97%85-%EA%B0%80%EB%A6%AC%EC%82%AC%EB%8B%88-Spring-boot-Security-1.-%EC%84%A4%EC%B9%98-%EB%B0%8F-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%84%A4%EC%A0%95.html
- DB에 일반 Account가 있고 OAuth 테이블과는 1:n 관계입니다. (하나의 계정에 여러개의 OAuth를 접합시킬 수 있다는 가정하에)
- OAuth로 로그인을 할 경우 유저ID와 전자우편을 가져옵니다.
- DB에 유저ID가 등록되지 않은 경우.
- OAuth 전용 가입페이지로 이동시킵니다. (간단히 생일정도만 적고 가입완료)
- DB에 유저ID가 등록되어 있는 경우.
- 해당 유저ID를 기준으로 Account를 찾아내서 위 직접 로그인의 Authentication 로 교체합니다.
- DB에 유저ID가 등록되지 않은 경우.
스프링에서 제공되는 OAuth 로그인
- 스프링 소셜을 이용하는경우.
- http://projects.spring.io/spring-social/ ``` xml
페이스북, 트위터, 링크드인등을 제공해줍니다.
장점 : 소셜기능도 매우 간단하게 사용할 수 있습니다.
단점 : 제공해주지 않는 서비스(네이버, 카카오등..)에 대한 구현이 상당한 노가다입니다..... ~~DI가.. 활용되지 못하는 느낌~~
결론 : 필자가 필요한건 단지 인증부 입니다. 이걸 다 구현하면 배보다 배꼽이 더 커질 수 있음으로 패스합니다.
이것으로 구현하실 분들은 org.springframework.social.security.SocialUserDetailsService 를 상속하여 시큐리티와 연계 구현하는 방법이 있습니다.
2. OAuth2 SSO(싱글 사인온)
- [https://spring.io/guides/tutorials/spring-boot-oauth2/#_social_login_simple
결론 : 예제에서도 볼 수 있듯 심플용으로 만들어진 것으로 기본적인 것들이 이미 구현되어 있는데 이름에서도 알 수 있듯 간단 구현용이라서 생략하겠습니다.
3. OAuth2 매뉴얼(수동)
``` xml
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
[https://spring.io/guides/tutorials/spring-boot-oauth2/ 소셜과 달리 설정파일을 주입하는 방식으로 설정파일 몇 줄과 코드 몇줄만 추가하면 소셜에선 제공해 주지 않는 네이버나 카카오등도 쉽게 확장할 수 있습니다.
구조
Account : 계정 AccountRole : 계정 권한 (계정1 : n 권한) [이 강의에선 쓰이지 않습니다.] AccountOauthClient : OAuth 계정 연계 (계정1 : n OAuth) web.configuration.security : 시큐리티 설정 web.configuration.security.direct : 직접 로그인 web.configuration.security.oauth2 : oauth2 로그인
직접 로그인 구현
스프링 시큐리티, 하이버네이트를 기준으로 구현됩니다. 주제가 OAuth 연동인만큼 일부분 생략하겠습니다. 스프링 시큐리티 강의는 아래 주소에서 볼 수 있습니다.
의존성
<!-- 스프링 시큐리티 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
룰 타입
- 기본적으로 USER 접속인지 OAUTH 접속인지를 구분합니다.
public enum RoleType { USER, OAUTH }
시큐리티
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired ApplicationContext context; @Override public void configure(WebSecurity web) throws Exception { // 메인페이지 : css나 js 같은것들도 여기에 포함시켜준다. // web.ignoring().antMatchers("/**"); } @Override protected void configure(HttpSecurity http) throws Exception { http // 강의 특성상 전부 허용으로 작업하겠습니다. .authorizeRequests() .antMatchers("/**") .permitAll() .and() .logout() .logoutUrl("/sign-out") .logoutSuccessUrl("/") .invalidateHttpSession(true) .and() .formLogin() .loginPage("/sign-in") .loginProcessingUrl("/sign-in/auth") .failureUrl("/sign-in?error=exception") .defaultSuccessUrl("/") .and() // 여기 나오는 sso.filter 빈은 다음장에서 작성합니다. // 이 장에서 실행을 확인하시려면 당연히 NPE 오류가 나니 아래 소스에 주석을 걸어주시기 바랍니다. .addFilterBefore((Filter)context.getBean("sso.filter"), BasicAuthenticationFilter.class); } }
DB 구조
@Entity(name="account") @Table(name="account") @EntityListeners(ColumnSaveOptionListener.class) @Data @Getter @Setter @ToString public class Account { @Id @Column(name="sn", nullable=false, unique=true) @SequenceGenerator(name="account_sn_seq", sequenceName="account_sn_seq", allocationSize=1) @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="account_sn_seq") long sn; @Column(name="mail", nullable=false, length=64, unique=true) String mail; @Column(name="auth", nullable=false, length=128) String auth; // 컬럼 생략... @OneToMany(mappedBy="sn", fetch = FetchType.LAZY) List<AccountRole> roles; }
계정 권한
@Entity(name="account_role") @Table ( name="account_role", uniqueConstraints=@UniqueConstraint(columnNames={"sn", "role"}) ) @Data @Getter @Setter @ToString public class AccountRole { @Id @Column(name="sn", nullable=false) long sn; @Column(name="role", nullable=false, length=64) String role; @Column(name="role_date") Date roleDate; }
계정 OAuth 연계 정보
@Entity(name="account_oauth_client") @Table ( name="account_oauth_client", uniqueConstraints=@UniqueConstraint(columnNames={"sn", "type"}) ) @Data @Getter @Setter @ToString public class AccountOauthClient { @Id @Column(name="sn", nullable=false) long sn; @Column(name="type", nullable=false, length=12) String type; @Column(name="id", nullable=false, length=64) String id; }
계정 리포지토리
public interface AccountRepository extends JpaRepository<Account, Long> { @Query("SELECT a FROM account a LEFT JOIN FETCH a.roles b WHERE a.mail = lower(:mail)") public Account findByMail(@Param("mail") String mail); @Query("SELECT a FROM account a LEFT JOIN FETCH a.roles b WHERE a.sn = (SELECT sn FROM account_oauth_client WHERE type = :type AND id = :id)") public Account findByOAuthId(@Param("type") String type, @Param("id") String id); }
계정 서비스
@Component public class AccountService { @Autowired AccountRepository accountRepository; public List<Account> findAll() { return accountRepository.findAll(); } public Account getAccountByMail(String mail) { return accountRepository.findByMail(mail); } public Account getAccountByOAuthId(String type, String id) { return accountRepository.findByOAuthId(type, id); } }
인증 설정 어뎁터
@Configuration public class AuthenticationConfig extends GlobalAuthenticationConfigurerAdapter { @Autowired UserDetailsService userDetailsService; @Override public void init(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean PasswordEncoder passwordEncoder() { // 예제입니다. // 본인이 사용하는 패스워드 인코더를 쓰시면됩니다. return new SaroPasswordEncoder(); } }
UserDetails
@Data @EqualsAndHashCode(callSuper=false) public class UserDetails extends User { private static final long serialVersionUID = 1L; @Getter private long sn; public UserDetails(Account account) { super ( account.getMail(), account.getAuth(), getAuthorities(account.getRoles()) ); sn = account.getSn(); } // 이 부분은 나중에 OAuth에서도 쓰이는 부분입니다.!! // 기본적으로 USER : RoleType.USER.toString() 를 주고 계정에 추가 권한을 줍니다. public static List<GrantedAuthority> getAuthorities(List<AccountRole> roles) { List<GrantedAuthority> list = new ArrayList<>(1); list.add(new SimpleGrantedAuthority(RoleType.USER.toString())); if (roles != null) { roles.stream().forEach((AccountRole role) -> { list.add(new SimpleGrantedAuthority(role.getRole())); }); } return list; } }
UserDetailsService
@Service public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService { @Autowired AccountService accountService; @Override public UserDetails loadUserByUsername(String ac) throws UsernameNotFoundException { Account account = accountService.getAccountByMail(ac); if (account == null) { throw new UsernameNotFoundException("sign-in fail"); } return new UserDetails(account); } }
추신
스프링 시큐리티를 통해 직접 로그인을 구현해 본신 분이라면 특별한점이 별로 없다는 것을 알 수 있습니다.
- 스프링 시큐리티 참고 참고해서 봐야할 부분은 아래와 같습니다.
- UserDetails.getAuthorities() 부분
- SecurityConfig의 .addFilterBefore((Filter)context.getBean(“sso.filter”), BasicAuthenticationFilter.class);
- DB 부분 여기 까지 작성했다면 로그인이 잘되는지 뷰를 만들고 .addFilterBefore((Filter)context.getBean(“sso.filter”), BasicAuthenticationFilter.class); 부분에 주석을 건 후 실행해 보시기 바랍니다. (물론 완료하고 주석을 해제합니다.)