[Spring Security] 웹과 모바일 서비스

WOOK JONG KIM·2022년 11월 30일
0

패캠_java&Spring

목록 보기
77/103
post-thumbnail

상황

  • 웹으로 잘 만든 사이트를 모바일로도 서비스해야 한다.
  • 그런데, 모바일 클라이언트 브라우저를 이용한 하이브리드 방식으로 개발을 한다. (세션이용이 가능함)
  • 그런데, 시간이 없어서 JWT 토큰 기반으로 만들기가 어렵다.
  • Basic 토큰을 이용해 기존 서비스를 api로 이용하고 싶다

시나리오

  • 선생님과 학생이 각각 로그인을 한다.
  • 선생님은 모바일을 통해 학생 리스트를 조회할 수 있다.

멀티체인을 구성하여 서비스

웹 리소스를 재사용하기 위해 student-teacher 웹모듈을 만듬
-> 저번에 잠시 다뤘던 내용 최종본

우선 각각 Port 9052, 9054로 지정후 실행시 각각의 페이지를 독립적으로 사용 가능하였음

또한 둘다 compile 의존성 추가

dependencies {
    implementation("$boot:spring-boot-starter-web")
    implementation("$boot:spring-boot-starter-thymeleaf")
    implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity5")
    implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect")
    compile project(":web-student-teacher")
}

Custom Login Filter

public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter {

    public CustomLoginFilter(AuthenticationManager authenticationManager){
        super(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String username = obtainUsername(request);
        username = (username != null) ? username : "";
        username = username.trim();
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        String type = request.getParameter("type");
        if(type == null || !type.equals("teacher")){
            // student
            StudentAuthenticationToken token = StudentAuthenticationToken.builder()
                    .credentials(username).build();
            return this.getAuthenticationManager().authenticate(token);
        }else{
            // teacher
            TeacherAuthenticationToken token = TeacherAuthenticationToken.builder()
                    .credentials(username).build();
            return this.getAuthenticationManager().authenticate(token);
        }
    }


}

MobileSecurityConfig

@Order(1) // MobileSecurityConfig에서 먼저 필터링 하기를 원하기 떄문
@Configuration
public class MobileSecurityConfig extends WebSecurityConfigurerAdapter {


    private final StudentManager studentManager;
    private final TeacherManager teacherManager;

    public MobileSecurityConfig(StudentManager studentManager, TeacherManager teacherManager) {
        this.studentManager = studentManager;
        this.teacherManager = teacherManager;
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(studentManager);
        auth.authenticationProvider(teacherManager);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/api/**")
                .csrf().disable()
                .authorizeRequests(request->
                    request.anyRequest().authenticated()
                )
                .httpBasic();
    }
}

Security Config

@Order(2)
@EnableWebSecurity(debug = false)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    private final StudentManager studentManager;
    private final TeacherManager teacherManager;

    public SecurityConfig(StudentManager studentManager, TeacherManager teacherManager) {
        this.studentManager = studentManager;
        this.teacherManager = teacherManager;
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(studentManager);
        auth.authenticationProvider(teacherManager);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        CustomLoginFilter filter = new CustomLoginFilter(authenticationManager());
        http
                .authorizeRequests(request->
                        request.antMatchers("/", "/login").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(
                        login->login.loginPage("/login")
                        .permitAll()
                        .defaultSuccessUrl("/", false)
                        .failureUrl("/login-error")
                )
                .addFilterAt(filter, UsernamePasswordAuthenticationFilter.class)
                .logout(logout->logout.logoutSuccessUrl("/"))
                .exceptionHandling(e->e.accessDeniedPage("/access-denied"))
                ;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
                ;
    }
}

student-teacher 모듈

MobTeacherController.java

@RestController
@RequestMapping("/api/teacher")
public class MobTeacherController {

    @Autowired
    private StudentManager studentManager;

    @PreAuthorize("hasAnyAuthority('ROLE_TEACHER')")
    @GetMapping("/students")
    public List<Student> students(@AuthenticationPrincipal Teacher teacher, Model model){
        return studentManager.myStudents(teacher.getId());
    }
}

Student.java

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Student {

    private String id;
    private String username;

    @JsonIgnore
    private Set<GrantedAuthority> role;

    private String teacherId;

}

@JsonIgnore 대신 보통 하나의 객체를 만들어 사용하기도 함

StudentManager.java

@Component
public class StudentManager implements AuthenticationProvider, InitializingBean {

    private HashMap<String, Student> studentDB = new HashMap<>();

    // UserNamePasswordAuthenticationToken으로 BasicAuthenticationToken을
    // 넘길것 이기에 양쪽을 처리할 수 있게 하엿음
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if(authentication instanceof UsernamePasswordAuthenticationToken){
            UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
            if(studentDB.containsKey(token.getName())){
                return getAuthenticationToken(token.getName());
            }
            return null;
        }
        StudentAuthenticationToken token = (StudentAuthenticationToken) authentication;
        if(studentDB.containsKey(token.getCredentials())){
            return getAuthenticationToken(token.getCredentials());
        }
        return null;
    }

    private StudentAuthenticationToken getAuthenticationToken(String id) {
        Student student = studentDB.get(id);
        return StudentAuthenticationToken.builder()
                .principal(student)
                .details(student.getUsername())
                .authenticated(true)
                .build();
    }


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

    public List<Student> myStudents(String teacherId){
        return studentDB.values().stream().filter(s-> s.getTeacherId().equals(teacherId))
                .collect(Collectors.toList());
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        Set.of(
                new Student("hong", "홍길동", Set.of(new SimpleGrantedAuthority("ROLE_STUDENT")), "choi"),
                new Student("kang", "강아지", Set.of(new SimpleGrantedAuthority("ROLE_STUDENT")), "choi"),
                new Student("rang", "호랑이", Set.of(new SimpleGrantedAuthority("ROLE_STUDENT")), "choi")
        ).forEach(s->
            studentDB.put(s.getId(), s)
        );
    }
}

authenticate의 코드를 보면 어떠한 Filter Chain Proxy를 거쳐올지 경우의 수를 대비

BasicAuthenticationFilter를 거쳐온 UserNameAuthenticationToken이 오는 경우
(즉 /api/** request 처럼 모바일에서 리퀘스트를 보내는 경우),

Custom Login Filter를 거쳐서 온 경우(Mobile에서 오는 request가 아니고, Custom Filter에 의해 각각의 타입에 따라 StudentToken이나, Teacher Token으로 Authentication Provider에 내려옴)

이러한 경우들에 대해 모두 다룰수 있게 코드 작성

TeacherManager.java

@Component
public class TeacherManager implements AuthenticationProvider, InitializingBean {

    private HashMap<String, Teacher> teacherDB = new HashMap<>();

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if(authentication instanceof UsernamePasswordAuthenticationToken){
            UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
            if(teacherDB.containsKey(token.getName())){
                return getTeacherAuthenticationToken(token.getName());
            }
            return null;
        }
        TeacherAuthenticationToken token = (TeacherAuthenticationToken) authentication;
        if(teacherDB.containsKey(token.getCredentials())){
            return getTeacherAuthenticationToken(token.getCredentials());
        }
        return null;
    }

    private TeacherAuthenticationToken getTeacherAuthenticationToken(String id) {
        Teacher teacher = teacherDB.get(id);
        return TeacherAuthenticationToken.builder()
                .principal(teacher)
                .details(teacher.getUsername())
                .authenticated(true)
                .build();
    }

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

    @Override
    public void afterPropertiesSet() throws Exception {
        Set.of(
                new Teacher("choi", "최선생", Set.of(new SimpleGrantedAuthority("ROLE_TEACHER")))
        ).forEach(s->
            teacherDB.put(s.getId(), s)
        );
    }
}

모바일에서 요청하는 경우를 테스트하는 코드

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MultiChainProxyTest {

    @LocalServerPort
    int port;

    TestRestTemplate testClient = new TestRestTemplate("choi", "1111");

    @DisplayName("1. choi:1 으로 로그인 해서 학생 리스트를 내려받는다.")
    @Test
    void test_1(){

        ResponseEntity<List<Student>> resp = testClient.exchange("http://localhost:" + port + "/api/teacher/students",
                HttpMethod.GET, null, new ParameterizedTypeReference<List<Student>>() {
                });
        assertNotNull(resp.getBody());
        assertEquals(3, resp.getBody().size());

        // System.out.println(resp.getBody());
    }
}
[Student(id=hong, username=홍길동, role=null, teacherId=choi), 
Student(id=rang, username=호랑이, role=null, teacherId=choi), 
Student(id=kang, username=강아지, role=null, teacherId=choi)]
profile
Journey for Backend Developer

0개의 댓글