IndexController
package com.cos.securityex01.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller // view 반환.
public class IndexController {
@GetMapping({"", "/"})
public String index() {
return "index"; // src/main/resources/templates/index.mustache
}
....
@GetMapping("/login")
public String login() {
return "loginForm";
}
}
login() 메서드만 @ResponseBody에노테이션을 제거하고, return "loginForm"으로 수정.loginForm.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
</head>
<body>
<h1>로그인 페이지</h1>
<hr/>
<form>
<input type="text" name="username" placeholder="Username"/><br>
<input type="password" name="password" placeholder="Password"/><br>
<button>로그인</button>
</form>
</body>
</html>

SecurityConfig에서 .anyRequest().permitAll()에 해당돼서 바로 접속 가능함.SecurityConfig
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable();
httpSecurity.authorizeRequests()
.antMatchers("/user/**").authenticated() // 해당 주소는 로그인이 필요.
.antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')") // 해당 주소로 접속 시 로그인뿐만 아니라 권한(admin, manager)도 필요함.
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')") // 해당 주소는 admin 권한만 들어갈 수 있음.
.anyRequest().permitAll() // 그외 다른 요청은 전부 허용.
.and().formLogin().loginPage("/login"); // 권한이 없는 주소로 요청을 하면 로그인 페이지로 이동되게 설정.
}
/user는 인증이 되어있어야되고, /manager, /admin은 특정 권한이 있어야 됨.

model 패키지를 만들고 User 클래스 생성.User
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.sql.Timestamp;
@Entity
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String email;
private String role; // ROLE_USER, ROLE_ADMIN
@CreationTimestamp
private Timestamp createDate;
}

DESC user;로 칼럼 확인.
IndexController
@GetMapping("/loginForm")
public String loginForm() {
return "loginForm";
}
@GetMapping("/joinForm")
public String joinForm() {
return "joinForm";
}
SecurityConfig
.and().formLogin().loginPage("/loginForm"); // 권한이 없는 주소로 요청을 하면 로그인 페이지로 이동되게 설정.
loginPage()만 변경.loginForm.html
<body>
<h1>로그인 페이지</h1>
<hr/>
<form>
<input type="text" name="username" placeholder="Username"/><br>
<input type="password" name="password" placeholder="Password"/><br>
<button>로그인</button>
</form>
<a href="/joinForm">회원가입을 아직 하지 않으셨나요?</a>
</body>

IndexController
@GetMapping("/joinForm")
public String joinForm() { // 회원가입 페이지.
return "joinForm";
}
joinForm이라는 템플릿이 없어서 404 에러가 발생함.
joinForm.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>회원가입 페이지</title>
</head>
<body>
<h1>회원가입 페이지</h1>
<hr/>
<form>
<input type="text" name="username" placeholder="Username"/><br>
<input type="password" name="password" placeholder="Password"/><br>
<input type="email" name="email" placeholder="Email"/><br>
<button>회원가입</button>
</form>
</body>
</html>

<form action="/join" method="POST">으로 수정.<form action="/join" method="POST">
<input type="text" name="username" placeholder="Username"/><br>
<input type="password" name="password" placeholder="Password"/><br>
<input type="email" name="email" placeholder="Email"/><br>
<button>회원가입</button>
</form>
IndexController
@PostMapping("/join")
@ResponseBody
public String join(User user) { // 회원가입
System.out.println(user);
return "join";
}


정상적으로 처리되어서 join문자열이 반환됐음.

System.out.println으로 찍은 콘솔도 정상적으로 값을 받아서 출력함.

UserRepository
public interface UserRepository extends JpaRepository<User, Integer> {
}
JpaRepository가 들고있음.UserRepository는 JpaRepository를 상속 받았기 때문에 @Repository에노테이션이 없어도 IoC가 됨.IndexController
@Autowired
private UserRepository userRepository;
@Autowired를 통해 DI(의존성 주입).IndexController
@PostMapping("/join")
@ResponseBody
public String join(User user) { // 회원가입
System.out.println(user);
user.setRole("ROLE_USER");
userRepository.save(user);
return "join";
}
setter를 이용해서 권한(Role) 값을 넣어줌.But이렇게 하면 안됨.SecurityConfig
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
BCryptPasswordEncoder를 빈으로 등록.메서드의 리턴되는 객체를 IoC로 등록해줌.IndexController
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired를 통해 DI해줌.IndexController
@PostMapping("/join")
public String join(User user) { // 회원가입
System.out.println(user);
user.setRole("ROLE_USER");
String rawPassword = user.getPassword();
String encodePassword = bCryptPasswordEncoder.encode(rawPassword);
user.setPassword(encodePassword);
userRepository.save(user);
return "redirect:/loginForm";
}
loginForm으로 리다이렉트 시켜주기.
loginForm으로 리다이렉트 됐음.

SecurityConfig
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable();
httpSecurity.authorizeRequests()
.antMatchers("/user/**").authenticated() // 해당 주소는 로그인이 필요.
.antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')") // 해당 주소로 접속 시 로그인뿐만 아니라 권한(admin, manager)도 필요함.
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')") // 해당 주소는 admin 권한만 들어갈 수 있음.
.anyRequest().permitAll() // 그외 다른 요청은 전부 허용.
.and().formLogin().loginPage("/loginForm") // 권한이 없는 주소로 요청을 하면 로그인 페이지로 이동되게 설정.
.loginProcessingUrl("/login") // '/login' 주소가 호출되면 시큐리티가 대신 로그인을 처리함. -> 그래서 컨트롤러에 login 메서드가 필요없음.
.defaultSuccessUrl("/"); // 로그인 성공 시 메인 페이지로 이동.
}
.loginProcessingUrl("/login")/login이 호출되면 시큐리티가 대신 로그인을 처리함./login을 처리하는 login 메서드가 필요없음..defaultSuccessUrl("/");/(메인 페이지)로 이동.loginForm.html
<form action="/login" method="POST">
<input type="text" name="username" placeholder="Username"/><br>
<input type="password" name="password" placeholder="Password"/><br>
<button>로그인</button>
</form>
/login으로 요청을 하면 시큐리티가 대신 로그인을 처리.
시큐리티가 /login 주소로 요청이 들어오면 대신 로그인을 처리함.
SecurityContext는 HTTP 세션에 저장되며, 이를 통해 사용자가 로그인 상태를 유지할 수 있음.
인증 객체가 Security Context에 저장되고, Security Context Hodler에 Security Context가 존재.SecurityContext안에 Authentication 객체가 존재하는지의 유무를 체크해서 인증여부를 결정함.SecurityContext는 사용자가 재접속하더라도 인증 당시의 데이터가 보존이 되어 있어야 함.세션에 저장된 SecurityContext 를 꺼내어 와서 SecurityContextHolder에 저장하게 되고전역으로 SecurityContext를 참조할 수 있게 하는 원리가 됨.Authentication 안에 User 정보가 있어야 됨.
PrincipalDetails class
public class PrincipalDetails implements UserDetails {
private User user; // 포함관계(합성(Composition))
public PrincipalDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetails 인터페이스를 구현했으므로 PrincipalDetails 타입의 객체를 UserDetails 타입으로 다룰 수 있음.PrincipalDetails를 Authentication안에 넣을 수 있음.public Collection<? extends GrantedAuthority> getAuthorities()String임.Collection<? extends GrantedAuthority>isAccountNonExpiredisAccountNonLockedisCredentialsNonExpiredEx) 일정 기간 동안 비밀번호를 변경하지 않아서 자격 증명이 만료된 경우.isEnabledEx) User모델에 마지막 로그인 시점 필드를 추가하고, 해당 메서드에서 user.getLoginDate()로 값을 추출해서Authentication 객체를 만들어야됨.@Service
public class PrincipalDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
loginProcessingUrl("/login")으로 해놨기 때문에 /login 요청이 오면 자동으로 UserDetailsService타입으로 IoC되어 있는 loadUserByUsername 메서드가 실행됨.UserDetailsService 타입의 빈을 찾아 loadUserByUsername 메서드를 호출.loadUserByUsername 메서드의 파라미터에 있는 username은 loginForm.html에 있는 name="username"과 매칭됨.<input type="text" name="username" placeholder="Username"/>loginForm.html에서 `name="username2"로 해버리면 매칭이 안됨.SecurityConfig(시큐리티 설정)에서 .usernameParameter("username2")로 지정해줘야됨.public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable();
httpSecurity.authorizeRequests()
....
.loginProcessingUrl("/login") // '/login' 주소가 호출되면 시큐리티가 대신 로그인을 처리함. -> 그래서 컨트롤러에 login 메서드가 필요없음.
.usernameParameter("username2")
.defaultSuccessUrl("/"); // 로그인 성공 시 메인 페이지로 이동.
}
↓/login이 호출됨.↓IoC컨테이너에서 UserDetailsService 타입으로 등록된 걸 찾음.PrincipalDetailsService↓loadUserByUsername 메서드를 호출.↓username을 받음.PrincipalDetailsService
@Service // 빈등록.
public class PrincipalDetailsService implements UserDetailsService {
@Autowired
private UserRepository repository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findByUsername(username);
return null;
}
}
@Autowired를 통해 DI 의존성 주입.UserRepository를 통해 findByUsername 메서드를 호출해서 해당 유저의 정보를 조회.findByUsername를 만들어줘야함. UserRepository
public interface UserRepository extends JpaRepository<User, Integer> {
User findByUsername(String username);
}
JPA Query MethodfindBy까지는 규칙, 그 뒤에 있는 Username은 문법.SELECT * FROM user WHERE username = ? 가 호출이 됨.?에는 파라미터에 있는 username이 삽입됨.)PrincipalDetailsService
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User userEntity = userRepository.findByUsername(username);
if (userEntity != null) {
return new PrincipalDetails(userEntity);
}
return null;
}
⊃ Authentication ⊃ UserDetailsAuthentication이 저장되는 곳은 SecurityContext.SecurityContextHolder는 SecurityContext를 감싸고 있는 클래스임.HttpSession은 인증 후 결과 정보가 있는 SecurityContext를 담아놓고 사용자가 계속 인증을 유지하기 위한 목적으로 사용되고 있지만 SecurityContextHolder가 HttpSession에서 SecurityContext를 꺼내어 다시 ThreadLocal에 저장하고 있음.SecurityContext가 최종 저장되는 곳은 ThreadLocal.HttpSession이 인증에 성공할 경우에 SecurityContext를 저장.SecurityContextHolder를 사용해서 HttpSession에서 SecurityContext를 꺼내어 ThreadLocal에 다시 저장.Authentication을 참조할 수 있도록 SecurityContextHolder.getContext().getAuthentication()와 같은 구문을 사용할 수 있게 됨.스프링 시큐리티에서는 해당 사용자가 인증이 되었는지 아닌지를 판별할 때 세션을 직접 참조하는 것이 아닌 ThreadLocal에 저장된 SecurityContext 객체를 꺼내어 이 속에 Authentication 객체가 존재하는지 여부를 보기 때문에 세션과는 직접적인 상관이 없음.토큰 방식을 사용할 수도 있기 때문에 세션에 의존적이지 않음.
localhost:8080/logout으로 로그아웃을 한 뒤에 로그인.

인덱스 페이지로 가짐.
SecurityConfig에 loginPage("/loginForm")로 설정하고 SuccessUrl을 /로 설정했으므로..and().formLogin().loginPage("/loginForm")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/");
다시 로그아웃 한 후, /user로 접속하면.


/loginForm으로 오게됨./user로 이동됨.

public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = 550L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
public Collection<GrantedAuthority> getAuthorities() {
return this.authorities;
}
public String getPassword() {
return this.password;
}
public String getUsername() {
return this.username;
}
public boolean isEnabled() {
return this.enabled;
}
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
public void eraseCredentials() {
this.password = null;
}
private static SortedSet<GrantedAuthority> sortAuthorities(Collection<? extends GrantedAuthority> authorities) {
Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet(new AuthorityComparator());
Iterator var2 = authorities.iterator();
while(var2.hasNext()) {
GrantedAuthority grantedAuthority = (GrantedAuthority)var2.next();
Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements");
sortedAuthorities.add(grantedAuthority);
}
return sortedAuthorities;
}
public boolean equals(Object obj) {
return obj instanceof User ? this.username.equals(((User)obj).username) : false;
}
public int hashCode() {
return this.username.hashCode();
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getName()).append(" [");
sb.append("Username=").append(this.username).append(", ");
sb.append("Password=[PROTECTED], ");
sb.append("Enabled=").append(this.enabled).append(", ");
sb.append("AccountNonExpired=").append(this.accountNonExpired).append(", ");
sb.append("credentialsNonExpired=").append(this.credentialsNonExpired).append(", ");
sb.append("AccountNonLocked=").append(this.accountNonLocked).append(", ");
sb.append("Granted Authorities=").append(this.authorities).append("]");
return sb.toString();
}
public static UserBuilder withUsername(String username) {
return builder().username(username);
}
public static UserBuilder builder() {
return new UserBuilder();
}
/** @deprecated */
@Deprecated
public static UserBuilder withDefaultPasswordEncoder() {
logger.warn("User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications.");
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
UserBuilder var10000 = builder();
Objects.requireNonNull(encoder);
return var10000.passwordEncoder(encoder::encode);
}
public static UserBuilder withUserDetails(UserDetails userDetails) {
return withUsername(userDetails.getUsername()).password(userDetails.getPassword()).accountExpired(!userDetails.isAccountNonExpired()).accountLocked(!userDetails.isAccountNonLocked()).authorities(userDetails.getAuthorities()).credentialsExpired(!userDetails.isCredentialsNonExpired()).disabled(!userDetails.isEnabled());
}
public static final class UserBuilder {
private String username;
private String password;
private List<GrantedAuthority> authorities;
private boolean accountExpired;
private boolean accountLocked;
private boolean credentialsExpired;
private boolean disabled;
private Function<String, String> passwordEncoder;
private UserBuilder() {
this.passwordEncoder = (password) -> {
return password;
};
}
public UserBuilder username(String username) {
Assert.notNull(username, "username cannot be null");
this.username = username;
return this;
}
public UserBuilder password(String password) {
Assert.notNull(password, "password cannot be null");
this.password = password;
return this;
}
public UserBuilder passwordEncoder(Function<String, String> encoder) {
Assert.notNull(encoder, "encoder cannot be null");
this.passwordEncoder = encoder;
return this;
}
public UserBuilder roles(String... roles) {
List<GrantedAuthority> authorities = new ArrayList(roles.length);
String[] var3 = roles;
int var4 = roles.length;
for(int var5 = 0; var5 < var4; ++var5) {
String role = var3[var5];
Assert.isTrue(!role.startsWith("ROLE_"), () -> {
return role + " cannot start with ROLE_ (it is automatically added)";
});
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
return this.authorities((Collection)authorities);
}
public UserBuilder authorities(GrantedAuthority... authorities) {
return this.authorities((Collection)Arrays.asList(authorities));
}
public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = new ArrayList(authorities);
return this;
}
public UserBuilder authorities(String... authorities) {
return this.authorities((Collection)AuthorityUtils.createAuthorityList(authorities));
}
public UserBuilder accountExpired(boolean accountExpired) {
this.accountExpired = accountExpired;
return this;
}
public UserBuilder accountLocked(boolean accountLocked) {
this.accountLocked = accountLocked;
return this;
}
public UserBuilder credentialsExpired(boolean credentialsExpired) {
this.credentialsExpired = credentialsExpired;
return this;
}
public UserBuilder disabled(boolean disabled) {
this.disabled = disabled;
return this;
}
public UserDetails build() {
String encodedPassword = (String)this.passwordEncoder.apply(this.password);
return new User(this.username, encodedPassword, !this.disabled, !this.accountExpired, !this.credentialsExpired, !this.accountLocked, this.authorities);
}
}
private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable {
private static final long serialVersionUID = 550L;
private AuthorityComparator() {
}
public int compare(GrantedAuthority g1, GrantedAuthority g2) {
if (g2.getAuthority() == null) {
return -1;
} else {
return g1.getAuthority() == null ? 1 : g1.getAuthority().compareTo(g2.getAuthority());
}
}
}
}
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}