상황
시나리오
멀티체인을 구성하여 서비스
웹 리소스를 재사용하기 위해 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)]