알쓸전컴(알아두면 쓸모있는 전자 컴퓨터)
spring security + CustomProvider + OAuth2 + JWT Server 설정 및 설명 (GrantTypes은 Password,refresh_token) 본문
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(256) PRIMARY 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,
'Web > Spring Framework tip' 카테고리의 다른 글
Transactional 옵션 설명 (0) | 2020.06.02 |
---|---|
QueryDSL Custom Funtion 등록 및 where 절에서 Index 사용하도록 하는 방법 (1) | 2020.04.06 |
Spring @Transactional 사용시 주의점 (0) | 2020.03.01 |
intellij 에서 json 을 class object Dto 자동 생성 Plugin 소개 (0) | 2019.10.28 |
Spring Boot, MyBatis 연동[멀티 데이터 베이스] (0) | 2019.07.08 |