티스토리 뷰

(구)게시판 프로젝트

로그인

_Bibidi 2021. 4. 16. 16:58

[ 로그인 기능이 필요한 이유 ]

 사용자에게 게시판 생성, 관리, 글쓰기, 댓글 달기, 사용자가 이모티콘을 구입하고 사용하는 등의 기능을 제공하려면 각 사용자를 구별할 수 있는 수단이 필요하다. 이러한 이유로 사용자 데이터는 관리되어야 하고 이 데이터를 이용해 로그인 기능을 제공해야 한다.

 

[ Spring Security Refereces ]

 * Reference

1. codevang.tistory.com/266 : 커스터마이징에 대해 상당히 깔금하게 잘 정리되어 있음. 이것만 있으면 밑에 찾은 레퍼런스들이 크게 필요없을 것 같다. 그리고 spring docs 등의 링크도 달려있다. 깃허브 들어가서 배우거나 힌트를 얻은 것도 꽤 있다.

2. docs.spring.io/spring-security/site/docs/5.0.7.RELEASE/reference/html/index.html : spring security docs 제일 최근에 나온 거랑 저자가 똑같다. 그 최신 문서는 보기가 힘든데, 이건 괜찮다. 현재 사용하고 있는 5.0.6버전이랑 가장 가까운 버전이다. security 설정할 때 쓰는 태그 같은 것들은 Expression-based access control 부분과 Security Namespace Appendix 부분에서 찾을 수 있다.

3. to-dy.tistory.com/86 : 스프링 시큐리티 로그인부터 로그인 실패 시 처리까지 어떻게 구현하는지 설명이 자세함.

4. okky.kr/article/382738 : 초보가 이해하는 스프링 시큐리티

 

[ Dependency ]

 - spring-security-web, config, core는 동일한 버전으로 맞춰야함.

 - spring-security-taglibs는 JSP에서 스프링 시큐리티 관련된 태그 라이브러리를 활용하기 위해 추가한 것

 - Spring Security는 단독으로 설정할 수 있기 때문에 별도로 security-context.xml을 따로 작성하는 것이 좋음.

 - 코드로 배우는 스프링 웹 프로젝트 책에 따르면 스프링 시큐리티를 설정할 때 5.0 네임스페이스에서 문제가 발생하기 때문에 4.2버전으로 사용하라 돼 있음.

 

[ Bean 설정 ]

 - 스프링 시큐리티 filter를 web.xml에서 바로 사용하려고 하면 제대로 작동하지 않는다. 빈이 제대로 설저오디지 않아서 발생한다.

 - 스프링 시큐리티 설정 파일을 찾을 수 있도록 web.xml에 security-context.xml을 로딩하도록 설정한다.

<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>/WEB-INF/spring/root-context.xml /WEB-INF/spring/root-context.xml</param-value>
</context-param>

 

 - 이후 security-context 파일에 최소한의 설정을 한다. 시큐리티가 동작하기 위해 Authentication manager와 스프링 시큐리티 시작 지점이 필요하다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:security="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">


	<security:http>
		<security:form-login />
	</security:http>
	
	<security:authentication-manager>
	
	</security:authentication-manager>
</beans>

 

 - 현재 설정

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:security="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<bean id="bcryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
	<bean id="userDetailsServiceImpl" class="com.bibidi.security.UserDetailsServiceImpl" />
	
	<security:http auto-config="true" use-expressions="true">
	
		<security:intercept-url pattern="/*" access="permitAll"/>
	
		<security:form-login 
			username-parameter="userId"
			password-parameter="userPassword"
			login-processing-url="/users/login"
			login-page="/users/login"
			/>
		
		<security:logout
			logout-url="/users/logout"
			logout-success-url="/"
			/>	
		
		<security:access-denied-handler error-page="/errors/accessError"/>
	</security:http>
	
	<security:authentication-manager>
	
		<security:authentication-provider user-service-ref="userDetailsServiceImpl">
			<security:password-encoder ref="bcryptPasswordEncoder"/>
		</security:authentication-provider>
	</security:authentication-manager>
</beans>

 

[ 스프링 시큐리티 커스터마이징 방식 ]

 - AuthenticationProvider를 직접 구현하는 방식

 새로운 프로토콜이나 인증 구현 방식을 직접 구현하는 경우에는 AuthenticationProvider 인터페이스를 직접 구현해서 사용함

 

 - 실제 처리를 담당하는 UserDetailsService를 구현하는 방식

 대부분의 경우에는 이 경우로 충분함.

 

 

[ URI, URL, URN ]

 * Reference

1. jinbroing.tistory.com/68

2. mygumi.tistory.com/139

 

 

[ URL 설계 ]

 - users라는 단어가 의미를 명확히 나타내나 ?

 - auth/login으로 구현한 사람도 있다. 이게 더 의미에 맞을 수도 있겠다. 그러나 일단은 users 쪽으로 유저 관련 기능과 페이지를 다 모아야겠다.

 

로그인 페이지  GET users/login?

로그인 POST users/login?

 

회원가입 페이지 GET users/signup

회원가입 POST users/signup

 

로그아웃 POST users/logout

 

 * goto 같은 걸로 원래 페이지로 돌아갈 수 있도록 구현할 수 있다.

 

 

[ REST api 관련 자료 링크 ]

 - 처음엔 간단한 줄 알았는데 알아야 할 게 너무 많다. 일단 대략적으로 지키면서 하자

 

 * Reference

1. blog.dreamfactory.com/best-practices-for-naming-rest-api-endpoints/

2. happyer16.tistory.com/entry/RESTful-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0

3. www.merixstudio.com/blog/best-practices-rest-api-development/

4. florimond.dev/blog/articles/2018/08/restful-api-design-13-best-practices-to-make-your-users-happy/

5. stackoverflow.blog/2020/03/02/best-practices-for-rest-api-design/

6. bcho.tistory.com/953?category=252770 - 사람들이 좀 많이 찾아보는 것 같음.

7. digitalbourgeois.tistory.com/54 - 위 사이트를 보고 다시 가다듬어 작성하신 분.

8. restfulapi.net/resource-naming/ - 조금 더 구체적인 예가 들어가 있음.

9. sanghaklee.tistory.com/57 - 이것도 구체적인 예시

10. sanghaklee.tistory.com/70 - 위와 같은 블로그

11. brunch.co.kr/@springboot/59

12. sustainable-dev.tistory.com/72 - auth/login 아이디어 참고한 곳

13. pjh3749.tistory.com/260?category=677077

 

 

 * Reference - RESTful api 게시판 구현 예제

1. sas-study.tistory.com/366

2. earth-95.tistory.com/50

3. kogle.tistory.com/114

4. developer.wordpress.org/rest-api/reference/users/#list-users : 이거 다 필요없고 여기서 일단 베껴오면 되겠는데? best practice

 

[ CSS Media Query ]

 - 메인 화면 인기 있는 게시판 여러 개 뿌리는 방식으로 구현하려고 할 때 찾아본 것. 옆에 side navigation 없애려고 찾아보니 media query가 나왔다.

 - 장치 환경이나 브라우저에 따라 CSS를 적용하는 기능이다. 나중에 프런트를 뜯어 고칠 때를 위해 메모해둔다.

 

 * Reference

1. developer.mozilla.org/ko/docs/Learn/CSS/CSS_layout/Media_queries

2. offbyone.tistory.com/121

3. skydoor2019.tistory.com/8

 

 

[ SVG ]

 - Scalable Vector Graphics의 약자로, 2차원 벡터 그래픽을 표현하기 위한 XML 기반 마크업 언어이다.

 - 아카라이브 홈페이지 왼쪽 상단 로고가 SVG로 이루어져있다.

 

 * Reference

1. velog.io/@sbyeol3/SVG%EB%A5%BC-%EA%B3%B5%EB%B6%80%ED%95%B4%EB%B3%B4%EC%9E%90-1-SVG%EB%9E%80

2. blog.naver.com/PostView.nhn?blogId=pjh445&logNo=220045621716 - SVG 링크 거는 법

3. a11y.gitbook.io/graphics-aria/svg-graphics/svg-paths-shape

 

 

[ Bezier Curve ]

 -SVG의 원리이다. 컴퓨터 그래픽스에서 봤던 거 같은데, 여기서도 이렇게 활용되고 있을 줄은 몰랐다.

 

 * Reference

1. en.wikipedia.org/wiki/B%C3%A9zier_curve#:~:text=A%20B%C3%A9zier%20curve%20(%2F%CB%88b,the%20bodywork%20of%20Renault%20cars.

2. ko.javascript.info/bezier-curve

 

[ Component ]

 - 리액트 같은 프레임워크 보니 프런트를 Component라는 단위로 나눠서 관리한다. 인기 게시판 div을 하나의 component처럼 만들어서 뿌려주도록 만들어도 될 것 같다. 그리고 이렇게 나누면 코드 반복이 적고 관리가 쉬워진다. JSP도 <%@ include file=""> 태그로 저렇게 나눈 단위를 추가시킬 수 있으니 이렇게 관리해야겠다.

 

 * Reference

1. han41858.tistory.com/15

2. medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

3. jeonghwan-kim.github.io/dev/2020/01/28/component-design.html

4. www.qcell.kr/think/?q=YToxOntzOjEyOiJrZXl3b3JkX3R5cGUiO3M6MzoiYWxsIjt9&bmode=view&idx=2155000&t=board

5. www.samsungsds.com/kr/insights/frameworks.html

 

 

[ Role ]

 - 내가 생각하는 사이트는 각 포럼마다 관리자, 운영자가 나뉘어져 있다. 사용자는 그냥 다 같은 사용자이다.

 - 각 역할이 할 수 있는 영역을 확실히 나눈다. 처음에는 관리자, 운영자, 유저 모두 글을 쓸 수 있으니 각 역할에 글을 쓸 수 있다는 권한을 넣어야 되나 생각했는데, 그건 아닌 것 같다. 보통 글을 쓰면 유저로서 글을 쓰는 거지 운영자로서 글을 작성하는 것이 아니다. 공지 같은 경우는 확실히 운영자나 관리자로서 글을 쓰는 건데, 이건 유저로서 글을 쓰고 그 글을 공지로 설정하는 것으로 처리할 수 있을 것 같다. 역할에 따라 기능을 어떻게 나눠야할지 고민해봐야한다.

 - (수정) 소문자를 모두 대문자로 바꾸고 'ROLE_'로 시작해야 한다. Spring Security http intercept-url의 hasRole 속성을 설정할 때 이름이 'ROLE_'로 시작하지 않으면 스프링이 자동으로 'ROLE_'을 추가한다. DefaultWebSecurityExpressionHandler에서 defaultRolePrefix를 수정하여 커스터마이즈할 수 있다고 하는데, 그러기엔 너무 귀찮다.

 - (수정) 명명 규칙에 게시판명이 아니라 게시판 아이디를 붙이는 것이 좋겠다. 만약에 게시판명이 바뀐다고 가정해보자. 그러면 역할 이름의 게시판명도 같이 바꿔야 한다. 그리고 role_name을 모두 대문자로 구성하게 할 건데, 사용자가 게시판 이름을 대소문자를 섞어서 만들어버리면 프런트에서 사용자가 가진 role_name과 게시판명을 비교해서 버튼을 보여주는 기능을 구현할 수 없을지도 모른다. 게시판 아이디를 붙이는 것으로 바꾸는 것이 좋겠다.

 

 - 명명 규칙

1.  ROLE_{역할}_{게시판 아이디}

 

 - 역할 리스트

1. ROLE_ADMIN : 전체 관리자

2. ROLE_USER : 일반 사용자

3. ROLE_ADMIN_{게시판 아이디} : 게시판 관리자

4. ROLE_MANAGER_{게시판 아이디} : 게시판 운영자

 

 * 읽어볼만한 글

1. docs.microsoft.com/ko-kr/aspnet/web-forms/overview/older-versions-security/roles/role-based-authorization-cs

 

 

[ Oracle data type ]

 - NUMBER(n1, n2) : n1은 전체 자릿수, n2는 소수점 자릿수. n2 생략 시 0으로 취급됨.

 - VARCHAR2(n) : 기본적으로 n은 byte 크기이다. 'n char'로 적으면 문자가 n개 들어갈 수 있도록 메모리를 할당한다. Oracle의 경우 4 * n byte 할당한다.

 

 - id의 경우 long을 쓸 것이고 long은 signed의 경우 2^63 - 1, unsigned의 경우 2^64까지이다. 2^63은 9.22e18이고 2^64은 1.8e19까지이다. 오라클에 number(20)으로 정하면 long을 충분히 커버할 수 있다.

 

 * Reference

1. coding-factory.tistory.com/416

2. docs.oracle.com/cd/E11882_01/server.112/e41084/sql_elements001.htm#SQLRF0021

 

 

[ UserVO ]

@Data
public class UserVO {
	private String userId;
	private String userPassword;
	private String userEmail;
	
	private String userNickname;
	private String userPicture;
	private Long userActivityPoint;
	
	private Date dateUserCreated;
	private Date dateUserEdited;
	
	private Boolean userEnabled;
	
	private List<String> roleList;
}

 - VO를 만들 때 고려해야 될 것들이 이렇게 많았나 싶다. 일단 primitivie type 피하는 것까지만 적용해본다.

 - primitive type을 피해 wrapper class를 이용하면 데이터베이스로부터 null 값을 받아 처리할 수 있다. userPicture의 경우 사용자가 설정하지 않으면 아무런 값이 없으니 null로 처리해야 되는데, 원시 타입은 null 값을 못 받으니 오라클에서 NVL 기능을 사용해서 어떤 특정 값으로 바꿔서 보내줘야 된다고 생각했다. 그런데 또 그렇게 하면 그 보내는 값을 어떻게 관리할 지가 문제이다. 나 혼자면 어디 적어두고 하면 되지만 현업에서 이런 점을 어떻게 다 하나하나 기억하겠냐며 머릿속이 복잡했는데, wrapper 클래스로 간단히 해결되네. 원시 타입 사용을 죄악시 하는 것도 이해가 간다.

 

 

 * Reference

1. velog.io/@livenow/Java-VOValue-Object%EB%9E%80

2. medium.com/@nicolopigna/value-objects-like-a-pro-f1bfc1548c72

3. velog.io/@jbb9229/VO-RO-ValueObject-ReferenceObject

4. velog.io/@livenow/Java-VOValue-Object%EB%9E%80

5. woowacourse.github.io/javable/post/2020-05-18-immutable-object/ : 불변 객체를 만드는 방법

 

[ Bycrypt 인코딩 ]

 

 * Reference

1. pjh3749.tistory.com/258

2. velog.io/@corgi/Spring-Security-PasswordEncoder%EB%9E%80-4kkyw8gi : Spring Security PasswordEncoder

3. octoperf.com/blog/2016/10/27/impl-classes-are-evil/#why-impl-is-bad : interface 구현할 때 -impl 쓰지 말라는 글

 

 

[ DB Table 생성 ]

create table users (
    user_number number(20) primary key,
    user_id varchar2(12) unique not null,
    user_password varchar2(72) not null,
    user_email varchar2(255) unique not null,
    user_nickname varchar2(16) unique not null,
    user_picture varchar2(255),
    user_activity_point number(20) default 0 not null,
    date_user_created date default sysdate not null,
    date_user_edited date default sysdate not null,
    user_enabled char(1) default '0' not null
);

create table roles (
    role_number number(20) primary key,
    role_name varchar2(50) unique not null
);

create table users_roles (
    user_number number(20) references users (user_number) on delete cascade,
    role_number number(20) references roles (role_number) on delete cascade,
    constraint users_roles_pk primary key (user_number, role_number)
);

create sequence seq_user;
create sequence seq_role;

 - sequence를 생성하는 이유는 db 내에서 id number를 처리하지 않고 외부에서 받는 형식으로 하면 db에 접근할 때 같은 id number를 가지고 올 수 있음. primary key가 걸려 있어 무결성에 문제가 생기진 않겠지만 어느 한 유저는 오류창을 볼 수밖에 없다. sequence를 이용하여 db 내부에서 처리해야 한다.

 - seq_user.nextval을 이용하면 된다.

 - 그런데 제약 조건 명명법도 따로 있나 모르겠다.

 

 

 * Reference

1. coding-factory.tistory.com/420 : sequence

2. multifrontgarden.tistory.com/31 : 컬럼 2개 기본키로 지정하는 법

3. araikuma.tistory.com/495 : 외래키 지정하는 법

4. blog.ycpark.net/entry/FOREIGN-KEY-%EC%99%80-CONSTRAINT-%EC%9D%98-%EC%82%AC%EC%9A%A9 : 제약조건 걸기

 

 

[ SQL ]

SELECT users.*, role_name
FROM users
	LEFT OUTER JOIN users_roles ON users.user_number = users_roles.user_number
	LEFT OUTER JOIN roles ON users_roles.role_number = roles.role_number
WHERE user_id = #{userId}

 - left outer join을 사용할 땐 기준 테이블에 다른 테이블을 cartesian product을 이용해서 조건에 따라 붙인다고 생각하면 편하다. 

 - inner join으로 권한까지 다 받아오려고 할 때 혹시나 user에게 역할이 부여되어 있지 않으면 user 데이터가 있어도 아무것도 반환되지 않는다. inenr join 되지 않는 튜플들이 다 잘리기 때문이다. 그래서 left outer join을 사용했다.

 - outer join 다음에 where로 조건을 걸면 먼저 조인을 다한 다음에 where를 적용하는지 아니면 where를 조건하는 만족하는 튜플을 먼저 찾은 뒤에 join을 하는지 모르겠다. 나중에 실행계획 분석하는 법을 익힌 다음에 튜닝해야겠다.

 

 

 * Reference

1. mine-it-record.tistory.com/64 : NULL값 치환해서 가져오는 방법

2. m.blog.naver.com/PostView.nhn?blogId=wideeyed&logNo=221435115388&proxyReferer=https:%2F%2Fwww.google.com%2F

    => left outer join이 어떻게 작동하는지 참고한 곳.

3. coding-factory.tistory.com/87 : 조인 설명을 두 집합을 가지고 하는데, 뭔 교집합, 합집합 그림을 가지고 설명하고 있다. cartesian product인데 저 그림 가지고 설명하는 게 맞나 ㅡㅡ ? 오히려 이 설명 보고 헷갈려서 고민했다. 두 set A, B에 union을 적용하면 A에 속하거나 B에 속하는 x를 모으고, intersection을 적용하면 A, B 모두에 속한 x를 찾는다는 의미이다. 각 집합의 element가 갖는 성질이 다른데, 어떻게 저 두 연산을 응용하여 적용하는 듯한 그림을 그려서 설명하는지 모르겠다. 그림만 보면 left outer join은 자기 자신을 반환하는 것처럼 보인다. 그래도 내가 이해를 잘못했을지도 모르니 나중에 다시 한 번 봐야겠다.

 

 

[ Access error page 작성 ]

 - Acess error 처리는 스프링 시큐리티가 기본으로 제공하는 기능과 AccessDeniedHandler를 직접 구현해서 처리하는 방법 두 가지가 있다. 페이지 이동만 처리할 거면 기본으로 제공하는 기능을 사용하며, 로그인 실패 횟수 증가 시 처리 같은 추가 로직이 필요한 경우 handler를 구현하여 처리한다.

 - 일단은 기본 페이지만 지정해놓고 나중에 추가 기능을 구현할 때 handler를 이용해 구현해보면 되겠다.

 - SPRING_SECURITY_403_EXCEPTION.getMessage() 기능 알아봐야 된다.

 

 * Reference

1. m.blog.naver.com/PostView.nhn?blogId=qbxlvnf11&logNo=221310171454&proxyReferer=https:%2F%2Fwww.google.com%2F

2. icunow.co.kr/404-error-page2/ : 에러페이지를 어떻게 구성하고 활용해야할지에 대한 글. 나중에 REST api 하고 같이 보면 좋을 듯.

3. cheese10yun.github.io/spring-guide-exception/ : 예외 관리 전략

4. velog.io/@aidenshin/Spring-Boot-Exception-Controller : 예외 컨트롤러 자동화

5. windorsky.tistory.com/entry/Controller%EC%9D%98-Exception-%EC%B2%98%EB%A6%AC

6. bigzero37.tistory.com/26?category=615516 : ??? 나중에 지울 가능성이 큼.

7. www.inflearn.com/questions/34787 : AccessDeniedHandler를 구현해서 사용해야 하는 경우가 어떤 때인지 알게된 곳

8. taesan94.tistory.com/124 : 로그인 실패 횟수 증가시 처리에 관한 부분. 여기 내에 참고 페이지가 또 있음.

9. kellis.tistory.com/46 : 나중에 지울 가능성이 큼. Auth 구현

10. zgundam.tistory.com/60 : 읽어보면 좋을 것 같다. 커스터마이징이 뭔가 많이 나온다.

 

[ Controller 작성 ]

@GetMapping("/login")
public String getLoginForm(String error, String logout, Model model) {
		
	if (error != null) {
		model.addAttribute("error", "Login Error. Check your Account");
	}
		
	if (logout != null) {
		model.addAttribute("logout", "See you again");
	}
		
	return "/users/loginForm";
}

@GetMapping("/accessError")
public void getAccessDeniedError(Authentication auth, Model model) {
	log.info("access Denied : " + auth);
		
	model.addAttribute("msg", "This is Access Denied message");
}

 

 - 이름 붙일 때 규칙은 HTTP request method name + 기능으로 이름 붙이면 될 것 같다.

 - ex) getLoginForm : Login form을 달라는 요청에 대한 처리, postLoginForm : Login form을 제출하는 요청에 대한 처리

 

 * Reference

1. doublesprogramming.tistory.com/211 : 게시판 만들기 예제. naming rule 참고

2. www.slipp.net/questions/79 : 2013년도지만 어느 정도 참고가 됨.

3. medium.com/the-resonant-web/spring-boot-2-0-project-structure-and-best-practices-part-2-7137bdcba7d3

4. m.blog.naver.com/PostView.nhn?blogId=magnking&logNo=220962933550&proxyReferer=https:%2F%2Fwww.google.com%2F : 신입들의 나쁜 습관

 

 

[ 로그인 페이지 작성 ]

<div class="panel-heading">
    <h3 class="panel-title">Sign In</h3>
</div>
<div class="panel-body">
    <h5><c:out value="${error}"/></h5>
    <h5><c:out value="${logout}"/></h5>
                    	
    <form role="form">
        <fieldset>
            <div class="form-group">
                <input class="form-control" placeholder="ID" name="userId" type="text" autofocus>
            </div>
            <div class="form-group">
                <input class="form-control" placeholder="Password" name="userPassword" type="password" value="">
            </div>
            <div class="checkbox">
                <label>
                    <input name="remember-me" type="checkbox" value="Remember Me">Remember Me
                </label>
            </div>
            <!-- Change this to a button or input when using this as a form -->
            <button class="btn btn-lg btn-success btn-block" formaction="/login" formmethod="post">Login</button>
        </fieldset>
        <input type="hidden" name="${_csrf.parameterName }" value="${_csrf.token}" />
    </form>
</div>

 - 제출 URI는 "/login"으로 해야 Spring Security 쪽으로 제출되어 로그인 작업을 하게 된다. Form 작성 시 주의해야 할 점은 name이다. username, password로 해줘야 제대로 인식하고 처리한다.

 - role은 웹 접근성을 위해 나왔으며 위젯,  구조 및 동작에 대한 의미 정보를 명확하게 전달한다.

 - HTML에 button tag가 있는지 몰랐다. 페이지 주석에 button이나 input으로 바꾸라고 되어 있는 거 보고 알았다. 생긴 게 button인데 input이라고 하고 제출하도록 만들기엔 찝찝해서, 조사 후 알맞게 바꿨다.

 - CSRF 토큰은 사이트간 위조 방지를 목적으로 특정한 값의 토큰을 사용하는 방식이다. 사용자의 요청에 대한 출처를 검사해 비정상적인 요청을 무시한다.

 - input name을 username, password로 해야하는 건 그게 기본값이라 그런 것이고 security 설정으로 바꿀 수 있다. 분명 이런 기능을 지원할 거라고 생각해서 찾았는데, 키워드가 잘못됐는지 이상하게 찾는 데 오래 걸렸다. 5번 reference를 봐도 되고 Spring Security 커스터마이징란의 1번 reference를 봐도 된다.

 

 * Reference

1. happycording.tistory.com/entry/HTML-Role-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC%EB%A7%8C-%ED%95%98%EB%8A%94%EA%B0%80 : role attribute에 대한 설명

2. m.blog.naver.com/PostView.nhn?blogId=hyonejun&logNo=150183645241&proxyReferer=https:%2F%2Fwww.google.com%2F

3. developer.mozilla.org/ko/docs/Web/HTML/Element/button : button tag에 대한 설명.

4. reiphiel.tistory.com/entry/spring-security-csrf : csrf 토큰 검증에 실패했을 때 처리 방법에 대한 내용이 있음.

5. to-dy.tistory.com/80 : 로그인 화면 커스터마이징

 

[ 로그아웃 ]

 - <logout> Attributes

  • delete-cookies A comma-separated list of the names of cookies which should be deleted when the user logs out.
  • invalidate-session Maps to the invalidateHttpSession of the SecurityContextLogoutHandler. Defaults to "true", so the session will be invalidated on logout.
  • logout-success-url The destination URL which the user will be taken to after logging out. Defaults to <form-login-login-page>/?logout (i.e. /login?logout)
  • Setting this attribute will inject the SessionManagementFilter with a SimpleRedirectInvalidSessionStrategy configured with the attribute value. When an invalid session ID is submitted, the strategy will be invoked, redirecting to the configured URL.
  • logout-url The URL which will cause a logout (i.e. which will be processed by the filter). Defaults to "/logout".
  • success-handler-ref May be used to supply an instance of LogoutSuccessHandler which will be invoked to control the navigation after logging out.

 - logout-url은 "users/logout", logout-success-url은 "/"로 지정해서 홈으로 가도록 했다.

 

 * Reference

1. velog.io/@devsh/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%84%B8%EC%85%98-Session-Management-%EC%BB%A4%EC%8A%A4%ED%84%B0%EB%A7%88%EC%9D%B4%EC%A7%95-%EA%B0%9C%EB%85%90-%EC%9D%B5%ED%9E%88%EA%B8%B0 : Spring Security Session Management 커스터마이징 개념, 아직은 어려워서 스킵 좋은 내용인 거 같으니 추후에 익힐 것.

2. xn--lg3bt3ss6d.com/9 : 링크 post 방식으로 보내도록 하는 방법.

 

[ 주석 다는 법 ]

    /**
     * 지정된 index에 위치하는 문자 반환. 
     * index의 범위는 0에서 length() - 1까지임.
     *
     * @param     index  얻고자 하는 문자의 인덱스 값
     * @return      지정된 index의 문자
     * @exception StringIndexOutOfRangeException
     *     index가 0에서 length()-1의 범위를 벗어남
     * @see       java.lang.Character#charValue()
     */
    public char charAt(int index) {
       ...
    }

 - 1번 reference에서 이런 방법이 있다는 걸 찾았다. 2번에 제대로 설명되어 있었음.

 

 * Reference

1. github.com/codevang/miniSpringWeb/blob/master/src/main/java/hs/spring/hsweb/controller/BoardController.java

2. www.gisdeveloper.co.kr/?p=1100 : 위와 같은 방법임. java docs와 관련되어 있는 것 같음.

3. danpatpang.github.io/tip/2018/04/12/Tip_java_comment/

 

 

[ MyBatis ]

 - root-context.xml Namespaces에서 mybatis 체크해주고 source에 <mybatis-spring:scan base-package="com.bibidi.mapper" />를 추가해줘야 한다.

 - MyBatis는 내부적으로 PreparedStatement를 이용해서 SQL을 처리하기 때문에 SQL parameter가 JDBC에서처럼 '?'로 치환되어 보인다. 제대로 값을 보려면 log4jdbc-log4j2 라이브러리를 사용해야 한다. 근데 이걸 설정하려면 properties 파일도 추가하고 root-context.xml의 hikariConfig도 수정해야 한다.

 - resultMap은 DB 필드값과 데이터를 받는 객체의 변수명이 다를 때 사용할 수 있다. <result property="" column="">에서 property는 객체의 변수명, column은 DB의 필드값을 입력하면 된다.

 - collection은 1 대 다 관계를 가진 데이터를 받을 때 사용한다.

 

<collection property="roleList" javaType="list" ofType="java.lang.String"/>

<collection property="roleList" javaType="list" ofType="java.lang.String">
	<result column="role_name"/>
</collection>

 - List<String>을 받을 때 reference를 보면 위와 같이 해야 될 것 같은데, 실제로는 아래처럼 해야 제대로 작동한다. 처음에 위처럼 작성했는데, 제대로 작동 안 해서 계속 찾아보니 아래와 같은 방법이 있었다. 이런 점에 나만 문제를 느낀 건 아닌지 github.com/mybatis/mybatis-3/issues/364를 보면 나와 같은 문제를 제기하는 사람이 있다.

 - 그리고 처음엔 위와 같이 작성했으나 나중에 데이터베이스에 role을 추가해야 된다는 점이 떠올랐다. 그러면 RoleVO를 정의해서 사용해야 하는데, 그럼 UserVO에서 role을 String으로 정의해서 사용하는 것은 옳지 않다. 새로 RoleVO를 정의하고 roleList의 타입을 바꿨다.

 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bibidi.mapper.UserMapper">

	<resultMap type="com.bibidi.domain.UserVO" id="userMap">
		<id property="userNumber" column="user_number"/>
		<result property="userNumber" column="user_number"/>
		<result property="userId" column="user_id"/>
		<result property="userPassword" column="user_password"/>
		<result property="userEmail" column="user_email"/>
		<result property="userNickname" column="user_nickname"/>
		<result property="userPicture" column="user_picture"/>
		<result property="userActivityPoint" column="user_activity_point"/>
		<result property="dateUserCreated" column="date_user_created"/>
		<result property="dateUserEdited" column="date_user_edited"/>
		<result property="userEnabled" column="user_enabled"/>
		<collection property="roleList" resultMap="roleMap"/>
	</resultMap>
	
	<resultMap type="com.bibidi.domain.RoleVO" id="roleMap">
		<id property="roleNumber" column="role_number"/>
		<result property="roleNumber" column="role_number"/>
		<result property="roleName" column="role_name"/>
	</resultMap>
	
	<select id="findByUserId" resultMap="userMap">
		SELECT
			users.*, roles.*
		FROM users
			LEFT OUTER JOIN users_roles ON users.user_number = users_roles.user_number
			LEFT OUTER JOIN roles ON users_roles.role_number = roles.role_number
		WHERE user_id = #{userId}
	</select>
</mapper>

 - 결국에는 이렇게 짰다.

 

 * Reference

1. twofootdog.github.io/Spring-DAO%EC%99%80-Mapper%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90/ : DAO를 이용한 케이스. 예전 구현 방식인 거 같은데 한 번 봐도 좋을 것 같다.

2. mybatis.org/mybatis-3/sqlmap-xml.html#Result_Maps : Mybatis Reference

3. app-dev.tistory.com/42

4. goodgid.github.io/Mybatis-Association-Collection-Part-1/

 

 

[ 각 context.xml 역할 ] 

 * Reference

1. thiago6.tistory.com/70 : context.xml 정리

2. jeong-pro.tistory.com/222 : servlet, context 정리

 

 

[ Test code 작성 ]

@RunWith(SpringRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml"})
@Log4j
public class UserMapperTests {

	@Setter(onMethod_ = @Autowired)
	private UserMapper userMapper;
	
	@Test
	public void testFindById() {
		UserVO user = userMapper.findByUserId("bibidi");
		
		log.info(user);
		
		user.getRoleList().forEach(role -> log.info(role));
	}
}

 - 

 

 * Reference

1. shlee0882.tistory.com/202 : Junit, anotation 정리

 

[ UserDetailsService 구현 ]

 - Authentication-Provider : Unless used with a ref attribute, this element is shorthand for configuring a DaoAuthenticationProvider. DaoAuthenticationProvider loads user information from a UserDetailsService and compares the username/password combination with the values supplied at login. The UserDetailsService instance can be defined either by using an available namespace element (jdbc-user-service or by using the user-service-ref attribute to point to a bean defined elsewhere in the application context). You can find examples of these variations in the namespace introduction.

 

 - 먼저 security-context.xml에 authentication-provider에 속성을 추가해줘야 한다. user-service-ref란 속성인데, 위 레퍼런스를 보면 UserDetailsService 인스턴스를 달아줘야 한다. 이를 이용해서 로그인 시 아이디와 비밀번호를 비교한다고 되어 있다.

 - UserDetailsService는 loadUserByUsername()이라는 하나의 추상 메서드만을 가지고 있으며, 리턴 타입은 UserDetails라는 타입이다. UserVO를 UserDetails 타입으로 변환해서 구현해도 되고 UserDetails를 구현한 User 클래스를 상속해 새로운 클래스를 만들어서 처리해도 된다.

 

@Log4j
public class UserDetailsServiceImpl implements UserDetailsService {

	@Setter(onMethod_ = @Autowired)
	private UserMapper userMapper;

	@Override
	public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
		
		log.warn("Load user by userId : " + userId);
		
		UserVO user = userMapper.findByUserId(userId);
		
		log.warn("queried by user mapper: " + user);
		
		return user == null ? null : new SecurityUser(user);
	}
	
}

 

@Getter
public class SecurityUser extends User {
	
	private static final long serialVersionUID = 1L;
	
	private UserVO user;
	
	public SecurityUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
		super(username, password, authorities);
	}
	
	public SecurityUser(UserVO userVO) {
		super(userVO.getUserId(), userVO.getUserPassword(), userVO.getRoleList().stream()
				.map(role -> new SimpleGrantedAuthority(role.getRoleName()))
				.collect(Collectors.toList()));
		
		this.user = userVO;
	}
}

 - UserDetailsService는 이해가 되는데, User는 이해가 잘 되지 않는다. 내가 구현한 SecurityUser가 User 클래스를 상속하기 때문에 부모 클래스의 생성자를 호출해야만 정상적인 객체를 생성할 수 있다고 한다. 이건 부모로부터 상속받은 변수들을 초기화해야 되니 필요한데, serialVersionUID는 왜 추가하는지 아직 모르겠다. 이에 대한 내용을 보충해야 한다.

 

 * Reference

1. shrtorznzl.tistory.com/26#:~:text=super%ED%82%A4%EC%9B%8C%EB%93%9C%EB%8A%94%20%EB%91%90%EA%B0%80%EC%A7%80%EB%A1%9C%20%EC%82%AC%EC%9A%A9%EB%90%9C%EB%8B%A4.&text=%EC%9E%90%EC%8B%9D%EC%9D%80%20%EB%B6%80%EB%AA%A8%EC%9D%98%20%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0,%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0%EB%A5%BC%20%EC%82%AC%EC%9A%A9%ED%95%A0%20%EC%88%98%20%EC%9E%88%EB%8B%A4.&text=%EC%A6%89%2C%20%EB%B6%80%EB%AA%A8%EC%99%80%20%EC%9E%90%EC%8B%9D%EC%9D%B4,%EA%B5%AC%EB%B6%84%ED%95%98%EA%B8%B0%20%EC%9C%84%ED%95%B4%20%EC%82%AC%EC%9A%A9%EB%90%9C%EB%8B%A4. : super을 사용하는 이유

2. to-dy.tistory.com/86 : 스프링 시큐리티 관련 구현 참고

3. mangkyu.tistory.com/114 : Java Stream API

 

 

[ 오류 ]

 - 접속한 아이디가 role_admin, role_user 권한을 가지고 있고 홈페이지 메인으로 접근할 때 필요한 권한을  hasRole('role_user')로 설정했으니 반드시 접속을 해야 하는데, 계속 access error가 뜨면서 거부됨. 로그는 다음과 같음

INFO : com.bibidi.controller.ErrorController - access Denied : org.springframework.security.authentication.UsernamePasswordAuthenticationToken@f9969de9: Principal: com.bibidi.security.SecurityUser@ad30b3d3: Username: bibidi; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: role_admin,role_user; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@21a2c: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: C4FE81F6E7014494EE6AA5C287E32FAD; Granted Authorities: role_admin, role_user

 - 해결 : 다른 블로그들 돌아다녀 보니 ROLE_ADMIN, ROLE_USER 이런 식으로 설정한 곳들은 다 성공함. hasRole에 문제가 있어서 조사해보니 hasRole에 설정된 이름이 ROLE_로 시작되지 않으면 스프링이 자동으로 ROLE_을 추가함. 그래서 Access Denied가 발생했던 것.

 

 * Reference

1. stackoverflow.com/questions/34928751/spring-security-hasrole-giving-error-403-access-is-denied : 첫 발견

2. offbyone.tistory.com/91 : hasRole 특징을 본 곳. spring security docs에서 찾으려고 해도 이상하게 안 나옴. 어디에 박혀있는지;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'(구)게시판 프로젝트' 카테고리의 다른 글

메인 화면  (0) 2021.04.27
회원가입  (0) 2021.04.26
프로젝트 생성 및 환경 설정  (0) 2021.04.15
데이터 모델링 2 (updated 21.05.07)  (0) 2021.04.10
데이터 모델링  (0) 2021.04.09
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함