본문 바로가기
공부/프로그래밍

[springboot, security] Authorization Server 실무에 써먹게 설정

by demonic_ 2020. 11. 15.
반응형

 

@EnableAuthorizationServer 을 설정하면 Authorization을 발급하는 서버로 지정된다.

이전 포스팅 참조.

lemontia.tistory.com/927

 

[springboot, oauth] Authorization Server(인증서버) 구축하기

OAuth2 역할은 크게 4가지로 분류된다 - Resource Owner - Authorization Server - Resource Server - Client :: Resource Owner는 유저를 뜻한다. 내가 만든 서비스를 이용하고자 하는 고객을 의미한다 :: Author..

lemontia.tistory.com

 

 

이번엔 DB(Mysql)을 이용한 토큰발급을 설정한다.

AuthorizationServerConfigurerAdapter 를 구현하는 클래스를 만들고, @EnableAuthorizationServer를 추가한다.

 

참고로 dataSource 가 빈으로 생성되도록 미리 설정되어 있어야 한다.

application.properties 설정

spring.datasource.driver-class-name=[Driver]
spring.datasource.url=[접속URL]
spring.datasource.username=[계정]
spring.datasource.password=[비밀번호]

 

AuthorizationServerConfigurerAdapter을 구현한 클래스

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Value("${oauth2.client_id}") // client id. 프로퍼티에 등록
    private String oauth2ClientId;

    @Value("${oauth2.secret}")  // secret. 프로퍼티에 등록
    private String oauth2Secret;

    private AuthenticationManager authenticationManager;

    private DataSource dataSource;

    public AuthorizationServerConfig(AuthenticationConfiguration authenticationConfiguration, DataSource dataSource) throws Exception {
        this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
        this.dataSource = dataSource;
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
        endpoints.tokenStore(jdbcTokenStore());
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient(oauth2ClientId)
                .secret("{noop}" + oauth2Secret)
                .autoApprove(true)
                .redirectUris("/oauth2/callback")
                .scopes("read", "write")
                .authorizedGrantTypes("password", "refresh_token")
        ;
    }

    public TokenStore jdbcTokenStore() {
        return new JdbcTokenStore(dataSource);
    }
}

 

인증하기 위한 호출(GET)

http://localhost:8080/oauth/authorize?client_id=testClient&response_type=code

다음처럼 username, password 를 물어본다.

 

 

로그인을 클릭하면 AuthenticationProvider로 이동하는데, 여기서 로그인 유효성을 검토하게 설정하면 된다.

여기서는 간단하게 다음처럼 설정한다.

@Configuration
@RequiredArgsConstructor
public class OauthAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();

        System.out.println("username = " + username);
        System.out.println("password = " + password);

        checkLogin(username, password);
        
        // 권한
        Collection<? extends GrantedAuthority> authorities = getAuthorities();

        return new UsernamePasswordAuthenticationToken(username, password, authorities);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }


    /**
     * 권한 추가
     */
    private Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
        grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_USER"));

        return grantedAuthorityList;
    }


    private void checkLogin(String username, String password) {
        if(username.equals("test") ) {
            throw new BadCredentialsException("아이디 또는 패스워드가 틀립니다");
        }
        if(BCryptImpl.isMatch(password, "$2a$10$Ihw2gtO0/XSpdJftWFkjlePlCM6HoIhg44iwdc3vUUMn3EnJ3OgES") == false) {
            throw new BadCredentialsException("아이디 또는 패스워드가 다릅니다");
        }
    }
}

 

여기서 로그인에 성공하면 oauth_access_token 테이블에 저장된다.(jdbc 로 연결 시)

 

하지만 이대로 하면 로그인 경로가 하나 더 있는 샘이라 보안이슈가 발생한다

때문에 oauth/authorize를 임의로 막을 필요가 있다

 

원본은 org.springframework.security.oauth2.provider.endpoint 패키지 내 AuthorizationEndpoint클래스에서 다음의 코드를 발견할 수 있다.

=> @RequestMapping(value = "/oauth/authorize")

 

다음과 같은 컨트롤러를 생성하여 여기를 바라보게 한다.

그리고 client_id와 secret을 파라미터로 받아서 비교한 뒤, 제대로 된 값을 호출했다면 Basic 토큰을 생성하여 리턴, 그러지 않으면 FAILED 를 리턴한다.

@RestController
public class AuthController {
    @Value("${oauth2.client_id}")
    private String oauth2ClientId;
    @Value("${oauth2.secret}")
    private String oauth2Secret;

    // GET 으로 했던 인증을 POST로 변경하면서 커스텀
    @PostMapping("/oauth/authorize")
    public String oauthAuthorize(@RequestBody @Valid RequestOauthAuthorize request) {
        if (request.getClientId().equals(oauth2ClientId) == false) {
            return "FAILED";
        }
        if(request.getSecret().equals(oauth2Secret) == false) {
            return "FAILED";
        }

        String credentials = request.getClientId()+":"+request.getSecret();
        String encodedCredentials = new String(Base64.encode(credentials.getBytes()));
        return "Basic " + encodedCredentials;
    }
}

 

이제 이전처럼 oauth/authorize 를 호출하면 다음의 에러를 리턴한다.

 

그리고 다음과 같이 호출하면 Basic 토큰을 리턴받는다.

(사실 이 토큰은 clientId와 secret을 합쳐 만든 것이지 특별한 암호화가 적용된 것은 아니다)

 

이제 Basic [토큰] 을 이용해서 oauth/token을 호출해 accessToken을 받아보자.

 

Authorization 에는 방금 받은 토큰인 Basic dGVzdENsaWVudDp0ZXN0U2VjcmV0 를 Header에 추가한다.

그리고 유저로 로그인하기 위한 username과 password를 파라미터에 추가하여 호출한다.

grant_type 은 password 로 입력

 

 

호출해보면 다음과 같이 리턴된다.

{
    "access_token": "62d85927-98e6-4623-96ff-8d38f9190d58",
    "token_type": "bearer",
    "refresh_token": "13f42190-00d0-491c-8d06-56d4a971eaea",
    "expires_in": 43199,
    "scope": "read write"
}

 

 

?) 왜 Basic [토큰] 이 필요할까?

accessToken을 받기 위해선 /oauth/token 를 호출해야 하는데 그러기 위해선 client_id와 secret으로 구성되어 있는 Basic 토큰이 필요하기 때문이다. 이것을 확인하기 위해 호출하는게 /oauth/authorize 이다.

 

 

끝.

반응형

댓글