스프링 시큐리티-JWT 로그인+로그아웃-(2) 권한이 여러 개 +동적 추가

이진우·2023년 9월 16일
0

스프링 학습

목록 보기
13/41
post-thumbnail

저번에는

저 번에는 @Enumberated로 역할을 받았었다. 하지만 이러니 여러 역할을 할당하기 힘들었기에 이번에는 role을 DB에 직접 저장하고 그로인해 여러 역할을 할당할수도,비즈니스 로직 실행중에 권한을 바꾸어 보도록 해보겠다.

지난 링크

https://velog.io/@dionisos198/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-JWT-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83with-Redis

수정사항

domain 영역

Member.java
@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를 통한 권한 설정이 아니고 엔티티를 이용해서 하게 만들었다. 또 동적으로 권한을 주는 것을 보여주기 위해서 상속을 사용하지 않았다.

MemberRole.java
@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사이의 다대다 관계를 풀었다.

Role.java
@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또한 따로 만들었다.

Ropository 영역

MemberRepository.java
public interface MemberRepository extends JpaRepository<Member,Long> {
    boolean existsByUserID(String userID);
    Optional<Member> findMemberByUserID(String userID);
}
MemberRoleRepository.java
public interface MemberRoleRepository extends JpaRepository<MemberRole,Long> {
    List<MemberRole> findByMember_UserID(String userID);

}
RoleRepository.java
public interface RoleRepository extends JpaRepository<Role,Long> {
    Role findByName(String name);

}

Repository 영역은 딱히 부연 설명을하지 않았다. 각자 필요해 보이는 것을 집어넣으면 될 듯하다.

Configuration영역

JwtUserDetailsService.java
@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의 권한을 채웠다.

TokenProvider.java
TokenProvider에는 아래 메서드를 추가하였다.
  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을 만드는 식으로 하였다.(인증객체 생성)

SecurityConfig.java
또한 저번이랑 다르게 Normal은 Normal의 영역만 접근이 가능하게 그리고, VIP는 VIP 영역만 접근이 가능하게 만든 후 Normal 에 VIP 권한을 추가 해보는 식으로 하면 둘다 접근할 수 있다는 것을 보여 보겠다.
  .antMatchers("/normal").access("hasRole('NORMAL')")
              .antMatchers("/vip").hasRole("VIP")

Service 영역

AuthService.java
@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을 만든다. 이제 이를 사용자에게 리턴해주면 된다.

Controller 영역

MemberController.java
@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부분만 추가해서 권한을 추가하는 코드를 넣었다.

AuthController.java
 @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를 생성하는 코드를 추가했다.

postman을 통한 test


위와 같이 ROLE_NORMAL이란 권한을 지정해주고

VIP 권한도 추가 해준다.


위와 같이 사용자가 권한을 고를 때 이것을 호출해주면 좋을 것같다.

이렇게 사용자가 권한을 선택할 수 도 있다.


로그인을 시도하면 아래와 같이 token 정보와 refreshToken이 나온다.


위처럼 권한이 normal 인 것은 무리없이 들어가고

권한이 vip인 것은 forbidden이다.
이제 여기다 VIP 권한을 추가해보겠다.


accessToken과 refreshToken에다가 추가할 authrityId를 넣어서 전달해주면 그 권한을 바탕으로 새로운 accessToken을 만들어냈다.


발급 받은 새로운 토큰으로 normal 에대한 접근 권한도 있고

VIP 에 대한 접근 권한도 있다.

DB의 중간테이블은


이런 형태를 결국 갖게 된다.

깃허브 링크

https://github.com/dionisos198/JwtLogin/tree/main

profile
기록을 통해 실력을 쌓아가자

0개의 댓글