보초라 야매로 작성했는데 잘 되실지는 모르겠습니다.
gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// MYSQL
implementation 'org.mariadb.jdbc:mariadb-java-client:3.0.4'
// MyBatis
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'
// log4jdbc
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
//security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.0.4.RELEASE'
testImplementation 'org.springframework.security:spring-security-test'
}
application.properties 설정
spring.datasource.url=jdbc:log4jdbc:mariadb://localhost:3306/DB이름?serverTimezone=UTC&characterEncoding=UTF-8&allowMultiQueries=true
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.username= 데이터베이스 유저 아이디
spring.datasource.password= 데이터베이스 유저 패스워드
mybatis.mapper-locations=/mapper/**/*.xml <- 매퍼를 넣을 경로
mybatis.configuration.map-underscore-to-camel-case=true
유저 아이디 root로 하면 안되더라구요 .
그래서 새로 DB유저를 생성한 후 권한을 주었습니다.
securityFilterChain
@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class WebSecurityConfig {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//유저 서비스
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
return http
.authorizeRequests()
.antMatchers("/login","/" /*"/members/**"*/).permitAll()
.antMatchers("/bread/**").permitAll()
.antMatchers("/static/**","/assets/**","/css/**","/imgs/**","/js/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.usernameParameter("memberid")
.passwordParameter("pwd")
.successHandler(new CustomLoginSuccessHandler())
.failureHandler(new CustomLoginFailHandler())
.and()
.logout()
.logoutUrl("/logout")
.invalidateHttpSession(true)
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
.and()
.csrf().disable()
.build();
}
}
- antMatchers (" 허용할 경로 ") .permitAll() <- 누구나 접근 가능
formLogin()
.loginPage("/ 커스텀 로그인페이지 경로")
.loginPage("/ form 서브밋 url")
.usernameParameter(" html의 로그인 아이디 name 값")
.usernameParameter(" html의 로그인 아이디 password 값")
.~~핸들러( 핸들러 커스텀하실거면 넣으시면됨 )
등등 방법이 많으니 검색하셔서 입맛대로 고치시면됨
프로젝트이름Application (이건 안하셔두됨)
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
저는 처음 페이지 접근시 로그인페이지로 갈 필요가 없어서
(exclude = {SecurityAutoConfiguration.class}) 를 넣어주었습니다.
MemberVO
@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberVO implements UserDetails {
private String membercode; //회원코드
private int authority; //권한식별
private String memberid; //아이디
private String pwd; //비밀번호
private String memberemail; //이메일
private String memberaddr; //기본주소
private String memberdaddr; //상세주소
private String portalcode; //우편번호
private String creatdate; //생성일
private String phone; //휴대전화
//권한
private List<String> auths;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for (String auth : this.auths) {
authorities.add(new SimpleGrantedAuthority(auth));
}
return authorities;
}
@Override
public String getPassword() {
return this.pwd;
}
@Override
public String getUsername() {
return this.memberid;
}
@Override
public boolean isAccountNonExpired() { //계정 만료 여부
return true;
}
@Override
public boolean isAccountNonLocked() { //계정 잠김 여부
return true;
}
@Override
public boolean isCredentialsNonExpired() { //비밀번호 만료 여부
return false;
}
@Override
public boolean isEnabled() { //계정의 활성화 여부
return false;
}
}
멤버 VO입니다. implement UserDetails를 해주었습니다.
님들 DB에 맞는 컬럼으로 작성해주세요.
CustomUserDetailsService
@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserService userService;
@Override
public UserDetails loadUserByUsername(String memberid) throws UsernameNotFoundException {
MemberVO memberVO = userService.getUserAccount(memberid);
if(memberVO == null){
throw new UsernameNotFoundException("유효하지 않는 로그인 정보입니다.");
}
return memberVO;
}
}
UserService는 조금 내리시면있어요
CustomAuthenticationProvider
@RequiredArgsConstructor
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final CustomUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String memberid = authentication.getName();
String pwd = (String) authentication.getCredentials();
MemberVO memberVO = (MemberVO) userDetailsService.loadUserByUsername(memberid);
if(!passwordEncoder.matches(pwd,memberVO.getPwd())){
throw new BadCredentialsException("비밀번호가 일치하지 않습니다.");
}
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
return new UsernamePasswordAuthenticationToken(memberid,pwd,authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); // ROLE_ <- 이게 안붙으면 인식을 못하더라구요
이부분은 권한 줄 방식을 입맛대로 하시면될듯
(DB에서 권한 조회 해 오시면될듯해요)
UserMapper
@Mapper
public interface UserMapper {
MemberVO getUserAccount (String memberid);
}
UserService
@RequiredArgsConstructor
@Service
public class UserService implements UserMapper{
private final UserMapper userMapper;
@Override
public MemberVO getUserAccount(String memberid) {
return userMapper.getUserAccount(memberid);
}
}
Mybatis User Mapper
<?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="패키지.UserMapper">
<select id="getUserAccount" resultType="패키지.MemberVO">
SELECT *
FROM member
WHERE memberid = #{memberid};
</select>
</mapper>
------------------------------------- 회원가입 처리 ---------------------------------------
MemberMapper
@Mapper
public interface MemberMapper {
void saveMember(MemberVO memberVO);
}
멤버 저장을 위한 멤버 매퍼입니다.
MemberService
@RequiredArgsConstructor
@Service
public class MemberService implements MemberMapper{
private final MemberMapper memberMapper;
@Override
public void saveMember(MemberVO memberVO) {
memberMapper.saveMember(memberVO);
}
}
멤버 서비스입니다.
MemberController
@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
private final BCryptPasswordEncoder passwordEncoder;
private final MemberService memberService;
@GetMapping("/test")
@ResponseBody
public String joinMember(){
MemberVO memberVO = new MemberVO();
memberVO.setMembercode("1"); <-이부분은 제가 따로 지정한거라 똑같이 하실필요x
memberVO.setAuthority(1); <-이부분은 제가 따로 지정한거라 똑같이 하실필요x
memberVO.setMemberid("admin");
memberVO.setPwd(passwordEncoder.encode("admin1234"));
memberService.saveMember(memberVO);
return "값이 저장되었습니다.";
}
}
로그인 테스트를 위한 어드민 계정을 생성해주는 컨트롤러입니다.
화면에 값이 저장되었다는 String 값을 출력해주기 위해 ResponseBody를 사용하였습니다.
spring security는 암호화된 비밀번호가 아니면 로그인을 할 수 없기 때문에
password를 암호화 시켜준 다음 저장합니다.
님들의 vo에 맞게 값을 세팅시켜주세요.
Mybatis Mapper
<?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="패키지.MemberMapper">
<insert id="saveMember" parameterType="패키지.MemberVO">
INSERT INTO member (membercode, authority, memberid, pwd, creatdate)
VALUES(#{membercode},#{authority},#{memberid},#{pwd},NOW())
</insert>
</mapper>
mapper namespace 는 아까 만든 MemberMapper가 있는 경로를 넣으시면됩니다.
parameterType 에 Vo가 있는 경로를 넣으시면됩니다.
로그인 html (Thymeleaf)
<form action="/login" id="form" method="post">
<div class="col mb-2 text-center">
<input class="ct-input" placeholder="아이디" name="memberid">
</div>
<div class="col mb-4 text-center">
<input class="ct-input" placeholder="비밀번호" name="pwd">
</div>
<div class="text-center fb mt-5" th:if="${error}">
<span th:text="${exception}"></span>
</div>
<div class="col mb-4 text-center mt2">
<button class="login-btn" type="submit"> 로그인 </button>
</div>
</form>
저는 로그인 실패시 에러메시지를 보여주기 위해
<div class="text-center fb mt-5" th:if="${error}">
<span th:text="${exception}"></span>
</div>
이부분을 썼습니다.
부가적 처리
CustomLoginFailHandler
@Slf4j
public class CustomLoginFailHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
String errormsg = null;
if(exception instanceof BadCredentialsException || exception instanceof InternalAuthenticationServiceException){
errormsg = "아이디 또는 미밀번호가 맞지 않습니다. 다시 확인해 주세요.";
}else if (exception instanceof InternalAuthenticationServiceException){
errormsg = "내부적으로 발생한 시스템 문제로 인해 요청을 처리할 수 없습니다. 관리자에게 문의해주세요.";
}else if (exception instanceof UsernameNotFoundException){
errormsg = "존재하지 않는 아이디 입니다.";
}else{
errormsg = "알 수 없는 이유로 로그인에 실패하였습니다. 관리자에게 문의하세요.";
}
log.info("error msg = {}", errormsg);
errormsg = URLEncoder.encode(errormsg,"UTF-8");
setDefaultFailureUrl("/login?error=true&exception="+errormsg);
super.onAuthenticationFailure(request,response,exception);
}
}
블로그 참고하여 작성했습니다.
에러메시지를 /login에 넘겨
@Controller
public class LoginViewController {
@GetMapping("/login")
public String login(@RequestParam(value = "error", required = false)String error,
@RequestParam(value = "exception", required = false) String exception, Model model){
model.addAttribute("error", error);
model.addAttribute("exception", exception);
return "/login";
}
}
컨트롤러에서 받은 후 모델로 보내줍니다.
<div class="text-center fb mt-5" th:if="${error}">
<span th:text="${exception}"></span>
</div>
아까 이 부분이 출력
CustomLoginSuccessHandler
@Slf4j
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("userId = {}",authentication.getName());
HttpSession session = request.getSession();
session.setAttribute("name", authentication.getName().toString());
//여기서 권한별로 로그인처리 해주면될듯ㅇㅇ
response.sendRedirect("/");
}
}
로그인 성공 후 처리
만드셨으면 아까 config에 등록해주시면됩니다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
return http
.authorizeRequests()
.antMatchers("/login","/" /*"/members/**"*/).permitAll()
.antMatchers("/bread/**").permitAll()
.antMatchers("/static/**","/assets/**","/css/**","/imgs/**","/js/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.usernameParameter("memberid")
.passwordParameter("pwd")
.successHandler(new CustomLoginSuccessHandler())
.failureHandler(new CustomLoginFailHandler())
.and()
.logout()
.logoutUrl("/logout")
.invalidateHttpSession(true)
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
.and()
.csrf().disable()
.build();
}