Skip to content

Commit

Permalink
Feat/login (#24)
Browse files Browse the repository at this point in the history
* feat: signup,login modify user controller, service

* feat: signup, login security config

* H2 database (#25)

* Test: Change database

* feat: login, JWT config

---------

Co-authored-by: JiSeong Lee <cocoquiet@knu.ac.kr>

* Update ci.yml

.env 파일 추가하도록 설정

---------

Co-authored-by: JiSeong Lee <cocoquiet@knu.ac.kr>
Co-authored-by: Yeonwoo Sea <62321953+Village-GG-Water@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 30, 2024
1 parent 73db79a commit 9ee620d
Show file tree
Hide file tree
Showing 14 changed files with 356 additions and 19 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ jobs:
java-version: '17'
distribution: 'temurin'

- name: Generate Environment Variables File
run: |
echo "JWT_SECRET=$JWT_SECRET" >> .env
env:
JWT_SECRET: ${{ secrets.JWT_SECRET }}

# Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies.
# See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
- name: Setup Gradle
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
.env

### STS ###
.apt_generated
Expand Down
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,23 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.security:spring-security-oauth2-jose'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0'
implementation 'jakarta.annotation:jakarta.annotation-api:2.1.0'
implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'io.github.cdimascio:java-dotenv:5.2.2'
}

tasks.named('test') {
Expand Down
48 changes: 47 additions & 1 deletion http/users.http
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,50 @@ Content-Type: application/json
}

### DELETE request
DELETE localhost:8080/users/2024123456
DELETE localhost:8080/users/2024123456

### Signup
POST http://localhost:8080/users/signup
Content-Type: application/json

{
"studentId": 1,
"name": "John Doe",
"password": "password123"
}

###

### Login
POST http://localhost:8080/users/login
Content-Type: application/json

{
"studentId": 1,
"password": "password123"
}

###

### Get All Users
GET http://localhost:8080/users

###

### Get User by ID
GET http://localhost:8080/users/1

###

### Update User
PUT http://localhost:8080/users/1
Content-Type: application/json

{
"name": "John Doe Updated"
}

###

### Delete User
DELETE http://localhost:8080/users/1
Binary file added local.mv.db
Binary file not shown.
53 changes: 53 additions & 0 deletions src/main/java/com/kert/config/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.kert.config;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;


import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;
private final SecurityUserService securityUserService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = getJWTFromRequest(request);

if (token != null && jwtTokenProvider.validateToken(token)) {
Long userId = jwtTokenProvider.getUserIdFromJWT(token);

try {
SecurityUser userDetails = securityUserService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (UsernameNotFoundException e) {
throw new RuntimeException("User not found");
}
}

filterChain.doFilter(request, response);
}

private String getJWTFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
62 changes: 62 additions & 0 deletions src/main/java/com/kert/config/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.kert.config;

import io.github.cdimascio.dotenv.Dotenv;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.context.annotation.Configuration;

import javax.crypto.SecretKey;
import java.util.Date;

@Configuration
public class JwtTokenProvider {

private final Dotenv dotenv = Dotenv.load();

private final String SECRET_KEY = dotenv.get("JWT_SECRET");
private final long EXPIRATION_TIME = 86400000;
private final SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_KEY));

// JWT 토큰 생성
public String generateToken(Long studentId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + EXPIRATION_TIME);

return Jwts.builder()
.setSubject(Long.toString(studentId))
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}

// JWT로부터 사용자 ID 추출
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();

return Long.parseLong(claims.getSubject());
}

// JWT 토큰의 유효성 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (SignatureException e) {
return false;
} catch (Exception e) {
return false;
}
}
}
94 changes: 89 additions & 5 deletions src/main/java/com/kert/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,108 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.kert.repository.AdminRepository;
import lombok.RequiredArgsConstructor;

import java.util.HashSet;
import java.util.Set;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final AdminRepository adminRepository;
private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

// api 테스트용 인증 비활성화 꼭 추후 수정할 것!!!

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.anyRequest().permitAll()
).sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
.requestMatchers("/", "/auth/**", "/oauth2/**", "/login", "/signup").permitAll()
.requestMatchers("/board", "/articles/**", "/mypage", "/dashboard/users").authenticated()
.requestMatchers("/dashboard/admin").hasRole("ADMIN")
.requestMatchers("/dashboard/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/h2-console/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.userInfoEndpoint(userInfo -> userInfo
.userAuthoritiesMapper(grantedAuthoritiesMapper())
)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
)
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions.disable()) // 프레임 옵션 비활성화
);;
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

private GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

authorities.forEach(authority -> {
if (authority instanceof OidcUserAuthority) {
mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
if (isAdmin((OidcUserAuthority) authority)) {
mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
} else if (authority instanceof OAuth2UserAuthority) {
mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
if (isAdmin((OAuth2UserAuthority) authority)) {
mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
} else {
mappedAuthorities.add(authority);
}
});

return mappedAuthorities;
};
}

@Bean
public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}

private boolean isAdmin(OidcUserAuthority oidcUserAuthority) {
Long userId = Long.valueOf((String) oidcUserAuthority.getAttributes().get("sub"));
return checkIfUserIsAdmin(userId);
}

private boolean isAdmin(OAuth2UserAuthority oauth2UserAuthority) {
Long userId = Long.valueOf((String) oauth2UserAuthority.getAttributes().get("sub"));
return checkIfUserIsAdmin(userId);
}

private boolean checkIfUserIsAdmin(Long userId) {
return adminRepository.findById(userId).isPresent();
}

}
9 changes: 5 additions & 4 deletions src/main/java/com/kert/config/SecurityUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ public SecurityUser(User user, Admin admin, Password password) {
this.admin = admin;
this.password = password;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
String role = "ROLE_USER";
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
if (admin != null) {
role = "ROLE_ADMIN";
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
return Collections.singletonList(new SimpleGrantedAuthority(role));
return authorities;
}

@Override
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/kert/config/SecurityUserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ public SecurityUser loadUserById(Long studentId) throws UsernameNotFoundExceptio

return new SecurityUser(user, admin, password);
}

public boolean isAdminById(Long studentId) {
Admin admin = adminRepository.findById(studentId).orElse(null);
return admin != null;
}
}
Loading

0 comments on commit 9ee620d

Please sign in to comment.