알쓸전컴(알아두면 쓸모있는 전자 컴퓨터)

spring security + CustomProvider + OAuth2 + JWT Server 설정 및 설명 (GrantTypes은 Password,refresh_token) 본문

Web /Spring Framework tip

spring security + CustomProvider + OAuth2 + JWT Server 설정 및 설명 (GrantTypes은 Password,refresh_token)

백곳 2020. 3. 4. 16:41

생각 보다 복잡 하여 생각을 정리하고자 자료를 작성함.

 

기본적으로 Spring security Data Flow 의 기본을 알고 있다는 바탕하에 자료를 작성함.

 

Principal 은 인증한 유저의 정보를 담고 있는 객체 

1. Principle 을 만들어 준다. 

@Data
public class Userinfo {
	private String uid;
	private String userName;
	private String groupName;
	private Integer role1;
	String jwttoken;
	String password;
}
public class UserinfoAdapter extends User {
	private Userinfo userinfo;

	public UserinfoAdapter(Userinfo account) {
		super(account.getUid(), account.getPassword(), authorities(account.getRole1(), account.getUid()));
		this.userinfo = account;
	}

	private static Collection<? extends GrantedAuthority> authorities(int role1, String uid) {
		return Arrays.asList(new SimpleGrantedAuthority("ROLE_" + role1), new SimpleGrantedAuthority("ROLE_" + uid));
	}

	public Userinfo getAccount() {
		return userinfo;
	}
}
@Component
public class UserService implements UserDetailsService {

	@Resource(name = "sqlSession")
	private SqlSession sqlSession;

	@Override
	public UserDetails loadUserByUsername(String UserID) throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		Userinfo userinfo = new Userinfo();
		userinfo.setUid(UserID);
		UserinfoMapper mapper = sqlSession.getMapper(UserinfoMapper.class);
		Userinfo tempuserinfo = mapper.selectByPrimaryKey(UserID);
		return new UserinfoAdapter(tempuserinfo);
	}

}

해당 객체는 UserDetailsService 을 상속 받아 생성 해야 한다. 

해당 객체를 @Component로 만들어 놓으면 OAuth2 Server Library 에서 

 

RefreshToken을 만들때 

 

해당 객체 에서 Bean 에 등록 되어 있는 UserDetailsService 을 가지고 와서 loadloadUserDetails 을 사용하여 

인증 객체에 Principal 을 넣어 줍니다. 

 

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	UserService accountService;

	@Bean
	public PasswordEncoder passwordEncoder() {
		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
	}

	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}



}

 

위와 같이 시큐 리티  설정을 합니다.

 

그 다음  Custom 한 로그인 인증을 하기 위해 직접 Provider를 제작 해 줍니다. 

 

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

	@Autowired
	private UserService userService;

	@Autowired
	AuthDao authdao;

	@Resource(name = "sqlSession")
	private SqlSession sqlSession;

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		String username = authentication.getName();
		String password = (String) authentication.getCredentials();

		UserinfoAdapter user = null;
		try {
			Userinfo userinfo = new Userinfo();
			userinfo.setUid(username);
			userinfo.setPassword(password);
            // 여기서 저는 다른 사이트에 로그인이 성공 하면 인증 완료 
			authdao.Login(userinfo);
			UserinfoMapper mapper = sqlSession.getMapper(UserinfoMapper.class);
			Userinfo tempinfo = mapper.selectByPrimaryKey(username);
			if (tempinfo == null) {
				mapper.insert(tempinfo);
			}
            //authdao.Login(userinfo); 내가 직접 만든 메소드
            //나의 경우에 authdao.Login(userinfo); 다른 사이트에 로그인을 해보고
            //성공했는지 실패 했는지로 로그인 성공여부를 판단한다. 
            //현재 Custome Provider는 일반 적인 경우는 아니다. 
            //보통은 아래 코드로 DB상에 패스워드를 받고 유져가 보내준 패스워드와
            //비교를 한다. 
			user = (UserinfoAdapter) userService.loadUserByUsername(username);

		} catch (UsernameNotFoundException e) {
			e.printStackTrace();
			throw new UsernameNotFoundException(e.getMessage());
		} catch (BadCredentialsException e) {
			e.printStackTrace();
			throw new BadCredentialsException(e.getMessage());
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e.getMessage());
		}
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(user, password,
				user.getAuthorities());
		result.setDetails(user);
		return result;

	}

	@Override
	public boolean supports(Class<?> authentication) {
        //다른 조건이 없으면 무조건 해당 provider 입장 
		return true;
	}

}

 

아래와 같이 Config를 수정 해 줍니다 

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	UserService accountService;

	//CustomProvider 주입 
    @Autowired
	CustomAuthenticationProvider customAuthenticationProvider;

	@Bean
	public PasswordEncoder passwordEncoder() {
		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
	}

	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	//해당 부분에서 등록
		auth.authenticationProvider(customAuthenticationProvider).userDetailsService(accountService);
	}

}

 

 

AuthorizationServerConfigurerAdapter(Oauth2 Server 설정 객체) 설정 하기 전에 DB 를 만들어 줍니다. 

이유는 해당 DB로 1단계로 서비스 인증이 이루어질 ClientId,SeretKey 인증에 사용할 정보를 저장할

DB를 만들어 줍니다.

 

oauth_client_details 는 OAuth2 라이브러리에서 인증에 사용하는 DB 입니다. 

 

create table oauth_client_details (

  client_id VARCHAR(256PRIMARY KEY,

  resource_ids VARCHAR(256),

  client_secret VARCHAR(256),

  scope VARCHAR(256),

  authorized_grant_types VARCHAR(256),

  web_server_redirect_uri VARCHAR(256),

  authorities VARCHAR(256),

  access_token_validity INTEGER,

  refresh_token_validity INTEGER,

  additional_information VARCHAR(2000),

  autoapprove VARCHAR(256)

);


이제 AuthorizationServerConfigurerAdapter 설정을 해준다. 

 

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

	@Autowired
	AuthenticationManager authenticationManager;

	@Autowired
	PasswordEncoder passwordEncoder;

	@Autowired
	UserService accountService;

	@Autowired
	private CustomAccessTokenConverter customAccessTokenConverter;

	@Autowired
	DriverManagerDataSource dataSource;

	@Bean
	public TokenStore tokenStore() {
		return new JwtTokenStore(accessTokenConverter());
	}

	@Bean
	public JwtAccessTokenConverter accessTokenConverter() {
		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		converter.setSigningKey("123");
		converter.setAccessTokenConverter(customAccessTokenConverter);
		return converter;
	}

	@Bean
	@Primary
	public DefaultTokenServices tokenServices() {
		DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
		defaultTokenServices.setTokenStore(tokenStore());
		defaultTokenServices.setSupportRefreshToken(true);
		return defaultTokenServices;
	}

	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
		security.passwordEncoder(passwordEncoder);
	}

	@Bean
	@Primary
	public JdbcClientDetailsService JdbcClientDetailsService(DataSource dataSource) {
		return new JdbcClientDetailsService(dataSource);
	}

//	@Autowired
//	private ClientDetailsService clientDetailsService;

//	@Override
//	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//
//		clients.withClientDetails(clientDetailsService);
//	}

	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//		clientId insert 코드  
		clients.jdbc(dataSource).withClient("ehssystem").authorizedGrantTypes("password", "refresh_token")
				.scopes("read", "write").secret(this.passwordEncoder.encode("ehs")).accessTokenValiditySeconds(7200)
				.refreshTokenValiditySeconds(86400).and().build();

	}

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
		tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
		endpoints.authenticationManager(authenticationManager).userDetailsService(accountService)
				.tokenStore(tokenStore()).tokenEnhancer(tokenEnhancerChain);
	}

	@Bean
	public TokenEnhancer tokenEnhancer() {
		return new CustomTokenEnhancer();
	}

}

 

위에 코드에서 필요한 추가적 객체는 아래와 같다. 아래 부분은 JWT을 사용하기 위한 객체 및 코드 이다. 

 

@Component
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {

	@Autowired
	UserService usersevice;

	@Autowired
	UserAuthenticationConverter userTokenConverter;

	@Override
	public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
    	//아래와 같이 usertoken 컨버터를 설정하지 않았을때 userdetailsService를 
        // OAuth2Authentication 가 UserdetailsService를 Bean 주입을 통하지 않기 때문에 기본  사용한다.
		super.setUserTokenConverter(userTokenConverter);
		OAuth2Authentication authentication = super.extractAuthentication(claims);
		return authentication;
	}

	@Bean
	public UserAuthenticationConverter getUserTokenConverter() {
    	//UserAuthenticationConverter 가 나의 UserdetailsService 객체를 사용 하도록 변경한다.
		DefaultUserAuthenticationConverter defaultUserAuthenticationConverter = new DefaultUserAuthenticationConverter();
		defaultUserAuthenticationConverter.setUserDetailsService(usersevice);
		UserAuthenticationConverter userTokenConverter = defaultUserAuthenticationConverter;
		return userTokenConverter;
	}

}

 

 

인가 할때 매번 CustomAccessTokenConverter 에서 authentication을 만들어서 넘겨 준다.

SecurityContextHolder.getContext().getAuthentication() 에 매번 해당 코드를 실행뒤 해당 코드에서 

생성된 authentication을 넘겨 받는다. 

ruturn OAuth2Authentication authentication = super.extractAuthentication(claims); 

만 하고 넘겨 주면 인가 부분에서  Principle 을 받을때 username 만 넘겨 받는다. 

기본적인 로직을 탈때 우리가 생성한 UsernameUserDetailsService 을 빈으로 주입 받지 않고 

자체 적으로 기본 UsernameUserDetailsService 을 생성 하고 loadUserByUsername을 실행 하여 

authentication을 넘겨 주기 때문이다. 

 

public class CustomTokenEnhancer implements TokenEnhancer {

	@Override
	public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
		Map<String, Object> additionalInfo = new HashMap<>();
		UserinfoAdapter userinfoAdapter = (UserinfoAdapter) authentication.getPrincipal();
		additionalInfo.put("uid", userinfoAdapter.getAccount().getUid());
		additionalInfo.put("groupName", userinfoAdapter.getAccount().getGroupName());
		additionalInfo.put("userName", userinfoAdapter.getAccount().getUserName());
		((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
		return accessToken;
	}
}

 

위에객체는 Jwt에 Cliam 을 Custom 하게 삽입 추가 할수 있다. 

 

 

이제 ResourceServerConfigurerAdapter 설정을 해준다. 인가를 설정하는 객체이다. 

본인은 메소드 시큐리티를 사용할것이기에 모두 허용해 주었다. 

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests().anyRequest().permitAll();
	}

}

 

그리고 나서 Spring Application을 실행해 보면 

 

insert into oauth_client_details (client_secret, resource_ids, scope, authorized_grant_types, 
web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, 
autoapprove, client_id) values ('{bcrypt}$2a$10$qAByZvjle.8GKNUUkH4ngeGU7BzsF7XPbN6UX7SctMvgg/mHB2rle','','read,write','password,refresh_token','','',7200,86400,'{}','','ehssystem')  

 

sql 쿼리문을 로그에 찍는 log4jdbc 을 사용한다면 위와 같은 쿼리가 실행 하는것을 알수 있다 .

 

해당 부분은 

public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
	...
	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//		clientId insert 코드  
		clients.jdbc(dataSource).withClient("ehssystem").authorizedGrantTypes("password", "refresh_token")
				.scopes("read", "write").secret(this.passwordEncoder.encode("ehs")).accessTokenValiditySeconds(7200)
				.refreshTokenValiditySeconds(86400).and().build();

	}
	...
}

해당 부분에 의해서 실행 되는것이다, 

 

실행이 되고 나면 

 

위와 같이 DB에 들어간 것을 볼수 있다 .

 

만약 다시 재실행 한다면 Priviate Key 중복 에러를 볼것이다. 

 

그러니 키가 생성 되었다면 

 

코드를 수정해 준다. 

 

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

	@Autowired
	AuthenticationManager authenticationManager;

	@Autowired
	PasswordEncoder passwordEncoder;

	@Autowired
	UserService accountService;

	@Autowired
	private CustomAccessTokenConverter customAccessTokenConverter;

	@Autowired
	DriverManagerDataSource dataSource;

	@Bean
	public TokenStore tokenStore() {
		return new JwtTokenStore(accessTokenConverter());
	}

	@Bean
	public JwtAccessTokenConverter accessTokenConverter() {
		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		converter.setSigningKey("123");
		converter.setAccessTokenConverter(customAccessTokenConverter);
		return converter;
	}

	@Bean
	@Primary
	public DefaultTokenServices tokenServices() {
		DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
		defaultTokenServices.setTokenStore(tokenStore());
		defaultTokenServices.setSupportRefreshToken(true);
		return defaultTokenServices;
	}

	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
		security.passwordEncoder(passwordEncoder);
	}

	@Bean
	@Primary
	public JdbcClientDetailsService JdbcClientDetailsService(DataSource dataSource) {
		return new JdbcClientDetailsService(dataSource);
	}

	@Autowired
	private ClientDetailsService clientDetailsService;

	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		//해당 부분으로 교체 
		clients.withClientDetails(clientDetailsService);
	}

//	@Override
//	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
////		clientId insert 코드  
//		clients.jdbc(dataSource).withClient("ehssystem").authorizedGrantTypes("password", "refresh_token")
//				.scopes("read", "write").secret(this.passwordEncoder.encode("ehs")).accessTokenValiditySeconds(7200)
//				.refreshTokenValiditySeconds(86400).and().build();
//
//	}

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
		tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
		endpoints.authenticationManager(authenticationManager).userDetailsService(accountService)
				.tokenStore(tokenStore()).tokenEnhancer(tokenEnhancerChain);
	}

	@Bean
	public TokenEnhancer tokenEnhancer() {
		return new CustomTokenEnhancer();
	}

}

 

이제 GlobalMethodSecurityConfiguration 를 설정 한다. 

 

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

	@Override
	protected AccessDecisionManager accessDecisionManager() {
		RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
		roleHierarchy.setHierarchy("ROLE_3 > ROLE_2 > ROLE_1");
		AffirmativeBased accessDecisionManager = (AffirmativeBased) super.accessDecisionManager();
		accessDecisionManager.getDecisionVoters().add(new RoleHierarchyVoter(roleHierarchy));
		return accessDecisionManager;
	}

}

 

같이 설정 하여 줍니다. accessDecisionManager override 은 Custom 하게 ROLE에 Level 을 둔것이므로 없어도 됩니다. 

 

example 로 아래와 같이 사용 하였습니다. 

	@RolesAllowed("ROLE_2")
	@PreAuthorize("#recode.getWriterid() == authentication.principal.getAccount().getUid()")
	@PostMapping(value = "/PartSafeCheck/v1/update/PartSafeChecks")
	int updatePartSafeChecks(@RequestBody Partsafecheck recode) {
		return partSafeCheckDao.updatePartSafeChecks(recode);
	}

 

참고 : https://www.baeldung.com/spring-security-oauth-jwt,

 

Comments