저 번에는 @Enumberated
로 역할을 받았었다. 하지만 이러니 여러 역할을 할당하기 힘들었기에 이번에는 role
을 DB에 직접 저장하고 그로인해 여러 역할을 할당할수도,비즈니스 로직 실행중에 권한을 바꾸어 보도록 해보겠다.
@Entity
@AllArgsConstructor
@Table(name = "member")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@DiscriminatorColumn
@Inheritance(strategy = InheritanceType.JOINED)
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
@Column(name="name",nullable = false)
protected String name;
@Column(name = "phone",nullable = false)
protected String phone;
@Column(name = "userID",nullable = false)
protected String userID;
@Column(name = "password",nullable = false)
protected String password;
@OneToMany(mappedBy = "member",cascade = CascadeType.ALL)
protected List<MemberRole> memberRoles=new ArrayList<>();
public void addRole(Role role){
MemberRole memberRole=MemberRole.createMemberRole(role);
this.memberRoles.add(memberRole);
memberRole.setMember(this);
}
public Member(String name,String phone,String userID,String password){
this.name=name;
this.phone=phone;
this.userID=userID;
this.password=password;
}
}
저번에랑 다른 점은 @Enumerated
를 통한 권한 설정이 아니고 엔티티를 이용해서 하게 만들었다. 또 동적으로 권한을 주는 것을 보여주기 위해서 상속을 사용하지 않았다.
@Entity
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Setter
public class MemberRole {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_ID")
private Role role;
public static MemberRole createMemberRole(Role role){
MemberRole memberRole=new MemberRole();
memberRole.setRole(role);
return memberRole;
}
}
MemberRole이란 중간테이블을 작성하여 Member와 Role사이의 다대다 관계를 풀었다.
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Role {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name",nullable = false)
private String name;
public Role(String name){
this.name=name;
}
}
역할의 이름을 가질 수 있는 Entity또한 따로 만들었다.
public interface MemberRepository extends JpaRepository<Member,Long> {
boolean existsByUserID(String userID);
Optional<Member> findMemberByUserID(String userID);
}
public interface MemberRoleRepository extends JpaRepository<MemberRole,Long> {
List<MemberRole> findByMember_UserID(String userID);
}
public interface RoleRepository extends JpaRepository<Role,Long> {
Role findByName(String name);
}
Repository 영역은 딱히 부연 설명을하지 않았다. 각자 필요해 보이는 것을 집어넣으면 될 듯하다.
@Service
@RequiredArgsConstructor
public class JwtUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
private final MemberRoleRepository memberRoleRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return memberRepository.findMemberByUserID(username)
.map(this::getUserDetails)
.orElseThrow(MemberNotFoundException::new);
}
public UserDetails getUserDetails(Member member) {
List<MemberRole> memberRoles =memberRoleRepository.findByMember_UserID(member.getUserID());
// 권한 정보를 GrantedAuthority 객체로 변환
Collection<? extends GrantedAuthority> authorities = memberRoles.stream()
.map(memberRole -> new SimpleGrantedAuthority(memberRole.getRole().getName()))
.collect(Collectors.toList());
return new User(member.getUserID(), member.getPassword(), authorities);
//user는 userDetails를 상속 받는다.즉 userDetails가 더 상위 개념
}
}
저번이랑 달라진 것은 UserDetails 의 메서드인데 저번에는 오직 한 개의 권한만 가정을 했기에 편했지만 이번에는 직접 Collection<? extends GrantedAuthority> authorities
를 만들어서 User의 권한을 채웠다.
public Authentication addRole(String token, Role role){
Claims claims=parseData(token);
List<SimpleGrantedAuthority> authorities = Arrays
.stream(claims.get(AUTHORIZATION_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
authorities.add(new SimpleGrantedAuthority(role.getName()));
User principal=new User(claims.getSubject(),"",authorities);
return new UsernamePasswordAuthenticationToken(principal,"",authorities);
}
비즈니스 로직 중 권한을 추가하고 싶을 때 token 정보로 부터 authorities
를 얻은 후 거기에다가 원하는 권한을 다시 추가한 후 다시 UsernamePasswordAuthenticationToken을 만드는 식으로 하였다.(인증객체 생성)
.antMatchers("/normal").access("hasRole('NORMAL')")
.antMatchers("/vip").hasRole("VIP")
@Transactional
public void MemberSignUp(SignUpDto signUpDto) {
Member member=getMember(signUpDto);
member.addRole(roleRepository.findById(signUpDto.getRoleId()).orElseThrow());
memberRepository.save(member);
}
private Member getMember(SignUpDto signUpDto){
if(memberRepository.existsByUserID(signUpDto.getUserID())){
throw new RuntimeException("중복 됩니다");
}
return new Member(signUpDto.getName(), signUpDto.getPhone(), signUpDto.getUserID(), passwordEncoder.encode(signUpDto.getPassword()));
}
이전에 작성했던 상속으로 각각 다른 객체를 만들 던 것을 지워버리고 Member로 통일을 해보았다. 이렇게 만들면 사용자가 권한을 클릭함에 따라 그에 따른 역할이 배정되어 저장될 것이다.
@Transactional
public void saveAuthority(RoleDto roleDto){
Role role=new Role(roleDto.getName());
roleRepository.save(role);
}
public List<RoleShowDto> roleShow(){
return roleRepository.findAll().stream().map(r->new RoleShowDto(r.getId(),r.getName())).collect(Collectors.toList());
}
위 메서드는 관리자가 직접 만든 새로운 권한을 지정할 수 있고 , 또 그 권한을 사용자에게 보여줄 수 있도록 한 메서드 이다.
@Transactional
public TokenResponseDto addAuthority(TokenAddAuthorityDto tokenAddAuthorityDto){
String accessToken=tokenAddAuthorityDto.getAccessToken();
String refreshToken=tokenAddAuthorityDto.getRefreshToken();
Authentication authentication= tokenProvider.getAuthentication(accessToken);
if(!redisTemplate.opsForValue().get(authentication.getName()).equals(refreshToken)){
throw new TokenNotCorrectException();
}
if (redisTemplate.opsForValue().get(authentication.getName())!=null){
redisTemplate.delete(authentication.getName());
}
Long expiration = tokenProvider.getExpiration(accessToken);
redisTemplate.opsForValue().set(accessToken,"logout",expiration,TimeUnit.MILLISECONDS);
Member findMember=memberRepository.findMemberByUserID(authentication.getName()).get();
Role findRole=roleRepository.findById(tokenAddAuthorityDto.getAuthorityId()).get();
findMember.addRole(findRole);
authentication= tokenProvider.addRole(accessToken,findRole);
TokenDto tokenDto=tokenProvider.createToken(authentication);
redisTemplate.opsForValue().set(authentication.getName(),tokenDto.getRefreshToken(),tokenDto.getRefreshTokenValidationTime(), TimeUnit.MILLISECONDS);
return new TokenResponseDto(tokenDto.getType(),tokenDto.getAccessToken(),tokenDto.getRefreshToken(),tokenDto.getAccessTokenValidationTime());
}
위 메서드가 진또배기 메서드인데 tokenAddAuthrityDto
로 부터 accessToken
,refreshToken
그리고 authorityId
를 얻어온다. 그러면 그 토큰에 포함되어 있는 accessToken의 정보를 바탕으로 인증 객체를 만든다(Authentication 부분). refreshToken이 제대로 있고 입력한 정보가 맞다면 redis에서 기존의 refreshToken을 지우고, 기존의 과거의 권한을 가진 accessToken은 redis에 넣어서 이 accessToken으로는 접근을 못하게 막는다.이제 과거의 토큰은 처리가 끝났으니 새로운 토큰을 새로운 권한으로 만들어보자. Member와 Role을 찾고 findMember.addRole
로 연관관계를 세팅해주자. 또한 tokenProvider.addRole
로 새로운 인증 객체를 만들고 그 인증 객체를 만들어서 새로운 accessToken과 refreshToken을 만든다. 이제 이를 사용자에게 리턴해주면 된다.
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
private final AuthService authService;
@GetMapping("/normal")
@ResponseStatus(HttpStatus.OK)
public String normal(){
return "normal";
}
@GetMapping("/vip")
@ResponseStatus(HttpStatus.OK)
public String vip(){
return "vip";
}
@PostMapping("/add/authority")
@ResponseStatus(HttpStatus.CREATED)
public TokenResponseDto addAuthority(@RequestBody TokenAddAuthorityDto tokenAddAuthorityDto){
return authService.addAuthority(tokenAddAuthorityDto);
}
}
기존의 코드에서 addAuthrity부분만 추가해서 권한을 추가하는 코드를 넣었다.
@PostMapping("/save/authority")
@ResponseStatus(HttpStatus.OK)
public void saveAuthority(@RequestBody RoleDto roleDto){
authService.saveAuthority(roleDto);
}
@GetMapping("/show/authority")
@ResponseStatus(HttpStatus.OK)
public List<RoleShowDto> showAuthority(){
return authService.roleShow();
}
@PostMapping("/save/member")
@ResponseStatus(HttpStatus.CREATED)
public void MemberSignUp(@RequestBody SignUpDto signUpDto){
authService.MemberSignUp(signUpDto);
}
권한을 저장하고 보여주고 , 상속이 아닌 Member를 생성하는 코드를 추가했다.
위와 같이 ROLE_NORMAL이란 권한을 지정해주고
VIP 권한도 추가 해준다.
위와 같이 사용자가 권한을 고를 때 이것을 호출해주면 좋을 것같다.
이렇게 사용자가 권한을 선택할 수 도 있다.
로그인을 시도하면 아래와 같이 token 정보와 refreshToken이 나온다.
위처럼 권한이 normal 인 것은 무리없이 들어가고
권한이 vip인 것은 forbidden이다.
이제 여기다 VIP 권한을 추가해보겠다.
accessToken과 refreshToken에다가 추가할 authrityId를 넣어서 전달해주면 그 권한을 바탕으로 새로운 accessToken을 만들어냈다.
발급 받은 새로운 토큰으로 normal 에대한 접근 권한도 있고
VIP 에 대한 접근 권한도 있다.
DB의 중간테이블은
이런 형태를 결국 갖게 된다.