diff --git a/pom.xml b/pom.xml
index d21e05f3efd0f24e65712ba5480a062529eb93a8..1eb9a854a0890ea5d4b43a2712a9688160209c4d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -170,6 +170,9 @@
1.3.33.4.6
+ 0.9.1
+ 2.3.1
+
3.8.13.2.0
diff --git a/web/pom.xml b/web/pom.xml
index ae705d60931a01339aca1a68d4d428a59f1deea9..3c1291b01e6e334f7c3210fce371effa0acbabfe 100644
--- a/web/pom.xml
+++ b/web/pom.xml
@@ -302,6 +302,17 @@
mail
+
+ io.jsonwebtoken
+ jjwt
+ ${jjwt.version}
+
+
+ javax.xml.bind
+ jaxb-api
+ ${jaxb.version}
+
+
com.navercorp.pinpoint
diff --git a/web/src/main/angular/angular.json b/web/src/main/angular/angular.json
index 02e116d02d0d0969fe3b988c148599b518f90f43..2dc534cd0574cddca92c506bf88c51abc14b5cae 100644
--- a/web/src/main/angular/angular.json
+++ b/web/src/main/angular/angular.json
@@ -21,6 +21,7 @@
"assets": [
"src/favicon.ico",
"src/assets",
+ "src/not_authorized.html",
{
"glob": "utils.js",
"input": "node_modules/intl-tel-input/build/js",
diff --git a/web/src/main/angular/src/not_authorized.html b/web/src/main/angular/src/not_authorized.html
new file mode 100644
index 0000000000000000000000000000000000000000..040414eb4dacf255659bc2c6ac8a1041bb8620d2
--- /dev/null
+++ b/web/src/main/angular/src/not_authorized.html
@@ -0,0 +1,93 @@
+
+
+
+ PINPOINT
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/PinpointBasicLoginConfig.java b/web/src/main/java/com/navercorp/pinpoint/web/PinpointBasicLoginConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..8b9207a7319b68b0adcb90cc706767596b868a6e
--- /dev/null
+++ b/web/src/main/java/com/navercorp/pinpoint/web/PinpointBasicLoginConfig.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2021 NAVER Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.navercorp.pinpoint.web;
+
+import com.navercorp.pinpoint.web.security.login.BasicLoginConstants;
+import com.navercorp.pinpoint.web.security.login.BasicLoginService;
+import com.navercorp.pinpoint.web.security.login.JwtRequestFilter;
+import com.navercorp.pinpoint.web.security.login.PreAuthenticationCheckFilter;
+import com.navercorp.pinpoint.web.security.login.SaveJwtTokenAuthenticationSuccessHandler;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
+
+/**
+ * @author Taejin Koo
+ */
+@Configuration
+@EnableWebSecurity
+@Profile("basicLogin")
+public class PinpointBasicLoginConfig extends WebSecurityConfigurerAdapter {
+
+ @Autowired
+ private BasicLoginService basicLoginService;
+
+ @Override
+ protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+ auth.eraseCredentials(false);
+ auth.userDetailsService(basicLoginService.getUserDetailsService());
+ }
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ // for common
+ http
+ .csrf().disable()
+ .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ .and()
+ .formLogin()
+ .successHandler(new SaveJwtTokenAuthenticationSuccessHandler(basicLoginService))
+ .and()
+ .httpBasic()
+ .and()
+ .logout()
+ .deleteCookies(BasicLoginConstants.PINPOINT_JWT_COOKIE_NAME);
+
+ // for admin
+ http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN")
+ .and()
+ .exceptionHandling()
+ .accessDeniedPage(BasicLoginConstants.URI_NOT_AUTHORIZED);
+
+ // for user
+ http.authorizeRequests().anyRequest().authenticated();
+
+ http.addFilterBefore(new JwtRequestFilter(basicLoginService), UsernamePasswordAuthenticationFilter.class);
+
+ http.addFilterBefore(new PreAuthenticationCheckFilter(), DefaultLoginPageGeneratingFilter.class);
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Override
+ @Bean
+ public AuthenticationManager authenticationManagerBean() throws Exception {
+ return super.authenticationManagerBean();
+ }
+
+}
\ No newline at end of file
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/WebApp.java b/web/src/main/java/com/navercorp/pinpoint/web/WebApp.java
index 7abfd74c9fa35e3c3633c7597afa7251ed765b53..6118f60ad936be05f915336ef28aed84246e31c4 100644
--- a/web/src/main/java/com/navercorp/pinpoint/web/WebApp.java
+++ b/web/src/main/java/com/navercorp/pinpoint/web/WebApp.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2021 NAVER Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.navercorp.pinpoint.web;
import com.navercorp.pinpoint.common.server.util.ServerBootLogger;
@@ -22,7 +38,7 @@ public class WebApp {
public static void main(String[] args) {
try {
- WebStarter starter = new WebStarter(WebApp.class, WebMvcConfig.class);
+ WebStarter starter = new WebStarter(WebApp.class, WebMvcConfig.class, PinpointBasicLoginConfig.class);
starter.start(args);
} catch (Exception exception) {
logger.error("[WebApp] could not launch app.", exception);
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/config/BasicLoginConfig.java b/web/src/main/java/com/navercorp/pinpoint/web/config/BasicLoginConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..8c45a03a918cc100bac55c8bbf4f180c9ef260d1
--- /dev/null
+++ b/web/src/main/java/com/navercorp/pinpoint/web/config/BasicLoginConfig.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2021 NAVER Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.navercorp.pinpoint.web.config;
+
+import com.navercorp.pinpoint.common.util.CollectionUtils;
+import com.navercorp.pinpoint.common.util.StringUtils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+import javax.annotation.PostConstruct;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Taejin Koo
+ */
+@Profile("basicLogin")
+@Configuration
+public class BasicLoginConfig {
+
+ private static final String DEFAULT_JWT_SECRET_KEY = "PINPOINT_JWT_SECRET";
+
+ private final int DEFAULT_EXPIRATION_TIME_SECONDS = new Long(TimeUnit.HOURS.toSeconds(12)).intValue();
+
+ private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+
+ @Value("${web.security.auth.user:}")
+ private List userIdAndPasswordPairList;
+
+ @Value("${web.security.auth.admin:}")
+ private List adminIdAndPasswordPairList;
+
+ @Value("${web.security.auth.jwt.secretkey:#{null}}")
+ private String jwtSecretKey = DEFAULT_JWT_SECRET_KEY;
+
+ private List userList;
+
+ private List adminList;
+
+ public List getUserList() {
+ return userList;
+ }
+
+ public List getAdminList() {
+ return adminList;
+ }
+
+ public String getJwtSecretKey() {
+ return jwtSecretKey;
+ }
+
+ public int getExpirationTimeSeconds() {
+ return DEFAULT_EXPIRATION_TIME_SECONDS;
+ }
+
+ @PostConstruct
+ public void setup() {
+ List userList = createUser(userIdAndPasswordPairList);
+ this.userList = userList;
+
+ List adminList = createAdmin(adminIdAndPasswordPairList);
+ this.adminList = adminList;
+
+ if (StringUtils.isEmpty(jwtSecretKey)) {
+ throw new IllegalArgumentException("'jwtSecretKey' may not be empty");
+ }
+ }
+
+ private List createUser(List idAndPasswordList) {
+ List users = createUserDetails(idAndPasswordList, "ROLE_USER");
+ return users;
+ }
+
+ private List createAdmin(List idAndPasswordList) {
+ List users = createUserDetails(idAndPasswordList, "ROLE_ADMIN");
+ return users;
+ }
+
+ private List createUserDetails(List idAndPasswordList, String role) {
+ if (CollectionUtils.isEmpty(idAndPasswordList)) {
+ return Collections.emptyList();
+ }
+
+ List users = new ArrayList<>(idAndPasswordList.size());
+
+ for (String idAndPassword : idAndPasswordList) {
+ List tokenize = StringUtils.tokenizeToStringList(idAndPassword, ":");
+ if (CollectionUtils.nullSafeSize(tokenize) == 2) {
+ String id = tokenize.get(0);
+ String password = tokenize.get(1);
+
+ User role_user = new User(id, passwordEncoder.encode(password), Arrays.asList(new SimpleGrantedAuthority(role)));
+ users.add(role_user);
+ }
+ }
+ return users;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("BasicLoginConfig{");
+ sb.append("userList=").append(userList);
+ sb.append(", adminList=").append(adminList);
+ sb.append('}');
+ return sb.toString();
+ }
+
+}
\ No newline at end of file
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/BasicLoginConstants.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/BasicLoginConstants.java
new file mode 100644
index 0000000000000000000000000000000000000000..b69855166dfed0c29130b438bd39fb885b9b2dea
--- /dev/null
+++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/BasicLoginConstants.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 NAVER Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.navercorp.pinpoint.web.security.login;
+
+/**
+ * @author Taejin Koo
+ */
+public final class BasicLoginConstants {
+
+ public static final String URI_MAIN = "/";
+
+ public static final String URI_LOGIN = "/login";
+
+ public static final String URI_NOT_AUTHORIZED = "/not_authorized.html";
+
+ public static final String PINPOINT_JWT_COOKIE_NAME = "pinpointJwt";
+
+}
\ No newline at end of file
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/BasicLoginService.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/BasicLoginService.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e283e5e4348d6e2c2916c1e4f8a9ee4679adf13
--- /dev/null
+++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/BasicLoginService.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2021 NAVER Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.navercorp.pinpoint.web.security.login;
+
+import com.navercorp.pinpoint.web.config.BasicLoginConfig;
+
+import io.jsonwebtoken.ExpiredJwtException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.Cookie;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Taejin Koo
+ */
+@Service
+@Profile("basicLogin")
+public class BasicLoginService {
+
+ private final PinpointMemoryUserDetailsService pinpointMemoryUserDetailsService;
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private final JwtService jwtService;
+
+ @Autowired
+ public BasicLoginService(BasicLoginConfig basicLoginConfig) {
+ PinpointMemoryUserDetailsService pinpointMemoryUserDetailsService = new PinpointMemoryUserDetailsService(basicLoginConfig);
+ this.pinpointMemoryUserDetailsService = pinpointMemoryUserDetailsService;
+
+ this.jwtService = new JwtService(basicLoginConfig);
+ }
+
+ public UserDetails getUserDetails(Cookie[] cookies) {
+ if (cookies == null) {
+ return null;
+ }
+
+ for (Cookie cookie : cookies) {
+ String name = cookie.getName();
+
+ if (BasicLoginConstants.PINPOINT_JWT_COOKIE_NAME.equals(name)) {
+ String pinpointJwtToken = cookie.getValue();
+
+ try {
+ Date expirationDate = jwtService.getExpirationDate(pinpointJwtToken);
+ if (expirationDate.getTime() > System.currentTimeMillis()) {
+ String userId = jwtService.getUserId(pinpointJwtToken);
+
+ UserDetails userDetails = pinpointMemoryUserDetailsService.loadUserByUsername(String.valueOf(userId));
+ if (userDetails != null) {
+ return userDetails;
+ }
+ } else {
+ logger.warn("This token already expired.");
+ }
+ } catch (ExpiredJwtException e) {
+ logger.warn("This token already expired. message:{}", e.getMessage(), e);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public Cookie createNewCookie(String userId) {
+ UserDetails userDetails = pinpointMemoryUserDetailsService.loadUserByUsername(userId);
+ if (userDetails == null) {
+ throw new IllegalArgumentException("Could not load User information.");
+ }
+
+ String token = jwtService.createToken(userDetails);
+ Cookie cookie = new Cookie(BasicLoginConstants.PINPOINT_JWT_COOKIE_NAME, token);
+ cookie.setPath("/");
+
+ long maxAge = TimeUnit.MILLISECONDS.toSeconds(jwtService.getExpirationTimeMillis());
+ cookie.setMaxAge(new Long(maxAge).intValue());
+ return cookie;
+ }
+
+ public UserDetailsService getUserDetailsService() {
+ return pinpointMemoryUserDetailsService;
+ }
+
+}
\ No newline at end of file
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/JwtRequestFilter.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/JwtRequestFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..a5273f8e43568793cc779c01e86ae552017c4d56
--- /dev/null
+++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/JwtRequestFilter.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2021 NAVER Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.navercorp.pinpoint.web.security.login;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * @author Taejin Koo
+ */
+public class JwtRequestFilter extends OncePerRequestFilter {
+
+ private final BasicLoginService basicLoginService;
+
+ private final WebAuthenticationDetailsSource webAuthenticationDetailsSource = new WebAuthenticationDetailsSource();
+
+ public JwtRequestFilter(BasicLoginService basicLoginService) {
+ this.basicLoginService = Objects.requireNonNull(basicLoginService, "basicLoginService");
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
+ Cookie[] cookies = httpServletRequest.getCookies();
+ if (cookies == null) {
+ filterChain.doFilter(httpServletRequest, httpServletResponse);
+ return;
+ }
+
+ UserDetails userDetails = basicLoginService.getUserDetails(cookies);
+ if (userDetails == null) {
+ filterChain.doFilter(httpServletRequest, httpServletResponse);
+ return;
+ }
+
+ SecurityContext securityContext = SecurityContextHolder.getContext();
+ if (securityContext != null && securityContext.getAuthentication() == null) {
+ UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
+ new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
+ usernamePasswordAuthenticationToken
+ .setDetails(webAuthenticationDetailsSource.buildDetails(httpServletRequest));
+ securityContext.setAuthentication(usernamePasswordAuthenticationToken);
+ }
+
+ filterChain.doFilter(httpServletRequest, httpServletResponse);
+ }
+
+}
\ No newline at end of file
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/JwtService.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/JwtService.java
new file mode 100644
index 0000000000000000000000000000000000000000..dda93d883c85e6a74cc892c60d06b6f08d190e7e
--- /dev/null
+++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/JwtService.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2021 NAVER Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.navercorp.pinpoint.web.security.login;
+
+import com.navercorp.pinpoint.common.util.Assert;
+import com.navercorp.pinpoint.common.util.StringUtils;
+import com.navercorp.pinpoint.web.config.BasicLoginConfig;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.JwtBuilder;
+import io.jsonwebtoken.JwtParser;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * @author Taejin Koo
+ */
+public class JwtService {
+
+ private final String KEY_CLAIMS_USER_ID = "userId";
+ private final String KEY_CLAIMS_USER_ROLE = "userRole";
+
+ private final JwtParser jwtParser;
+
+ private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
+
+ private final String secretKey;
+
+ private final long expirationTimeMillis;
+
+ public JwtService(BasicLoginConfig basicLoginConfig) {
+ String secretKey = basicLoginConfig.getJwtSecretKey();
+ if (StringUtils.isEmpty(secretKey)) {
+ throw new IllegalArgumentException("secretKey may not be empty");
+ }
+ this.secretKey = secretKey;
+
+ Assert.isTrue(basicLoginConfig.getExpirationTimeSeconds() > 0, "expirationTimeSeconds must be '>= 0'");
+ this.expirationTimeMillis = TimeUnit.SECONDS.toMillis(basicLoginConfig.getExpirationTimeSeconds());
+
+ JwtParser jwtParser = Jwts.parser();
+ jwtParser.setSigningKey(secretKey);
+ this.jwtParser = jwtParser;
+ }
+
+ public String createToken(UserDetails userDetails) {
+ Map claims = new HashMap<>();
+
+ Collection extends GrantedAuthority> authorities = userDetails.getAuthorities();
+
+ List collect = authorities.stream().map(e -> ((GrantedAuthority) e).getAuthority()).collect(Collectors.toList());
+
+ claims.put(KEY_CLAIMS_USER_ID, userDetails.getUsername());
+ claims.put(KEY_CLAIMS_USER_ROLE, collect);
+
+ return createToken(claims);
+ }
+
+ private String createToken(Map claims) {
+ JwtBuilder jwtBuilder = Jwts.builder();
+
+ jwtBuilder.setClaims(claims);
+ jwtBuilder.setIssuedAt(new Date(System.currentTimeMillis()));
+ jwtBuilder.setExpiration(new Date(System.currentTimeMillis() + expirationTimeMillis));
+ jwtBuilder.signWith(signatureAlgorithm, secretKey);
+
+ return jwtBuilder.compact();
+ }
+
+ public String getUserId(String token) {
+ Jws claimsJws = jwtParser.parseClaimsJws(token);
+ Claims body = claimsJws.getBody();
+
+ return body.get(KEY_CLAIMS_USER_ID, String.class);
+ }
+
+ public Date getExpirationDate(String token) {
+ Jws claimsJws = jwtParser.parseClaimsJws(token);
+ Claims body = claimsJws.getBody();
+ return body.getExpiration();
+ }
+
+ public long getExpirationTimeMillis() {
+ return expirationTimeMillis;
+ }
+
+}
\ No newline at end of file
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/PinpointMemoryUserDetailsService.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/PinpointMemoryUserDetailsService.java
new file mode 100644
index 0000000000000000000000000000000000000000..55addade3194e5a3c7e12940ac74c8dd2ec2bdaf
--- /dev/null
+++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/PinpointMemoryUserDetailsService.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2021 NAVER Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.navercorp.pinpoint.web.security.login;
+
+import com.navercorp.pinpoint.web.config.BasicLoginConfig;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author Taejin Koo
+ */
+@Service
+@Profile("basicLogin")
+public class PinpointMemoryUserDetailsService implements UserDetailsService {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private final Map userDetailsMap;
+
+ @Autowired
+ public PinpointMemoryUserDetailsService(BasicLoginConfig basicLoginConfig) {
+ Map userDetailsMap = new HashMap<>();
+
+ final List userList = basicLoginConfig.getUserList();
+
+ for (UserDetails user : userList) {
+ userDetailsMap.put(user.getUsername(), user);
+ }
+
+ if (logger.isDebugEnabled()) {
+ Collection userRoleUserNameList = userList.stream().map(user -> user.getUsername()).collect(Collectors.toList());
+ logger.debug("Has been registered {} that has USER role.", userRoleUserNameList);
+ }
+
+ List adminList = basicLoginConfig.getAdminList();
+ for (UserDetails admin : adminList) {
+ userDetailsMap.put(admin.getUsername(), admin);
+ }
+
+ if (logger.isDebugEnabled()) {
+ Collection adminRoleUserNameList = adminList.stream().map(user -> user.getUsername()).collect(Collectors.toList());
+ logger.debug("Has been registered {} that has ADMIN role.", adminRoleUserNameList);
+ }
+
+ this.userDetailsMap = Collections.unmodifiableMap(userDetailsMap);
+ }
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ return userDetailsMap.get(username);
+ }
+
+}
\ No newline at end of file
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/PreAuthenticationCheckFilter.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/PreAuthenticationCheckFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..aba76fb9bc38e9aaa1cbac0b08cf87ba4aa25cf1
--- /dev/null
+++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/PreAuthenticationCheckFilter.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2021 NAVER Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.navercorp.pinpoint.web.security.login;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.filter.GenericFilterBean;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * @author Taejin Koo
+ */
+public class PreAuthenticationCheckFilter extends GenericFilterBean {
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ SecurityContext securityContext = SecurityContextHolder.getContext();
+ if (securityContext == null) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ Authentication authentication = securityContext.getAuthentication();
+ if (authentication == null) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ if (authentication.isAuthenticated() && ((HttpServletRequest) request).getRequestURI().equals(BasicLoginConstants.URI_LOGIN)) {
+ ((HttpServletResponse) response).sendRedirect(BasicLoginConstants.URI_MAIN);
+ } else {
+ chain.doFilter(request, response);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/SaveJwtTokenAuthenticationSuccessHandler.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/SaveJwtTokenAuthenticationSuccessHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..e75a8900976b61aff78a8a14038e5e7425b8aeac
--- /dev/null
+++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/SaveJwtTokenAuthenticationSuccessHandler.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2021 NAVER Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.navercorp.pinpoint.web.security.login;
+
+import com.navercorp.pinpoint.common.util.Assert;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * @author Taejin Koo
+ */
+public class SaveJwtTokenAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ private final BasicLoginService basicLoginService;
+
+ public SaveJwtTokenAuthenticationSuccessHandler(BasicLoginService basicLoginService) {
+ this.basicLoginService = Assert.requireNonNull(basicLoginService, "basicLoginService");
+ }
+
+ @Override
+ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+ logger.debug("onAuthenticationSuccess() started");
+
+ String userId = authentication.getName();
+
+ Cookie newCookie = basicLoginService.createNewCookie(userId);
+ response.addCookie(newCookie);
+
+ response.setStatus(HttpStatus.OK.value());
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+
+ response.sendRedirect(BasicLoginConstants.URI_MAIN);
+ return;
+ }
+
+}
\ No newline at end of file
diff --git a/web/src/main/resources/applicationContext-web.xml b/web/src/main/resources/applicationContext-web.xml
index 46d165f115051817e6cab0e05104809f03ddb4dd..f1620537cd263b3e8f0ebfbba82b6362404c0928 100644
--- a/web/src/main/resources/applicationContext-web.xml
+++ b/web/src/main/resources/applicationContext-web.xml
@@ -201,4 +201,10 @@
+
+
+
+
+
+
diff --git a/web/src/main/resources/pinpoint-web-root.properties b/web/src/main/resources/pinpoint-web-root.properties
index 8db99d7d2edaea39bfad0ef3a30d3586961d891d..badbec34369cc66999a8259c08a27f2f17d2d3d7 100644
--- a/web/src/main/resources/pinpoint-web-root.properties
+++ b/web/src/main/resources/pinpoint-web-root.properties
@@ -84,4 +84,10 @@ websocket.allowedOrigins=
# If you have own downloadUrl information, please include the pinpoint version and downloadUrl information.
# default value is the github's pinpoint page.
web.installation.pinpointVersion=
-web.installation.downloadUrl=
\ No newline at end of file
+web.installation.downloadUrl=
+
+# Declares user:password in pinpoint-web.properties. (You can declare multiple people using,.)
+# Role (User : Can use whole function except for admin rest api, Admin : Can use whole function)
+#web.security.auth.user=alice:foo, bob:bar
+#web.security.auth.admin=eve:baz
+#web.security.auth.jwt.secretkey=PINPOINT_JWT_SECRET
\ No newline at end of file