JAX-RS 및 Jersey를 사용한 REST 토큰 기반 인증 구현 방법
Jersey에서 토큰 기반 인증을 활성화하는 방법을 찾고 있습니다.나는 어떤 특정한 틀도 사용하지 않으려고 노력하고 있다.가능할까요?
내 계획은 사용자가 내 웹 서비스에 등록하고 내 웹 서비스가 토큰을 생성하여 클라이언트에 보내면 클라이언트는 이를 보유한다는 것입니다.그러면 클라이언트는 각 요구에 대해 사용자 이름과 비밀번호 대신 토큰을 전송합니다.
@PreAuthorize("hasRole('ROLE')")
토큰이 유효한지 여부를 확인하기 위해 데이터베이스에 대한 요구가 많다고 생각했을 뿐입니다.
아니면 필터를 작성하지 않고 각 요청에 파라미터 토큰을 넣습니까?따라서 각 API는 처음에 토큰을 확인하고 그 후에 리소스를 취득하기 위해 무언가를 실행합니다.
토큰 기반 인증 구조
토큰 기반 인증에서는 클라이언트는 토큰이라는 데이터 조각과 하드 자격 정보(사용자 이름 및 암호 등)를 교환합니다.각 요구에 대해서, 클라이언트는 하드 credential을 송신하는 대신에, 서버에 토큰을 송신하고, 인증과 인가를 실시합니다.
한마디로 토큰에 기초한 인증방식은 다음 절차를 따릅니다.
- 클라이언트는, 자격 정보(사용자명과 패스워드)를 서버에 송신합니다.
- 서버는 credential을 인증하고 유효한 경우 사용자용 토큰을 생성합니다.
- 서버는 이전에 생성된 토큰을 사용자 ID 및 만료 날짜와 함께 일부 저장소에 저장합니다.
- 서버는 생성된 토큰을 클라이언트에 전송합니다.
- 클라이언트는 각 요청에서 토큰을 서버로 전송합니다.
- 서버는, 각 요구로 착신 요구로부터 토큰을 추출합니다.토큰을 사용하면 서버는 인증을 수행하기 위해 사용자 세부사항을 검색합니다.
- 토큰이 유효한 경우 서버는 요청을 수락합니다.
- 토큰이 유효하지 않은 경우 서버는 요청을 거부합니다.
- 인증이 실행되면 서버는 인가를 수행합니다.
- 서버는 토큰을 새로 고치는 엔드포인트를 제공할 수 있습니다.
JAX-RS 2.0(Jersey, RESTEasy 및 Apache CXF)으로 수행할 수 있는 작업
이 솔루션에서는 JAX-RS 2.0 API만을 사용하여 벤더 고유의 솔루션을 사용하지 않습니다.따라서 JAX-RS 2.0 구현(Jersey, RESTEASY, Apache CXF 등)과 연동해야 합니다.
기반을 사용하는 웹 하지 않고 어플리케이션의 EE를 할 수 .web.xml
커스텀 인증입니다.
사용자 이름과 비밀번호로 사용자 인증 및 토큰 발행
자격 증명(사용자 이름 및 암호)을 수신하고 검증하는 JAX-RS 리소스 메서드를 만들고 사용자에게 토큰을 발급합니다.
@Path("/authentication")
public class AuthenticationEndpoint {
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response authenticateUser(@FormParam("username") String username,
@FormParam("password") String password) {
try {
// Authenticate the user using the credentials provided
authenticate(username, password);
// Issue a token for the user
String token = issueToken(username);
// Return the token on the response
return Response.ok(token).build();
} catch (Exception e) {
return Response.status(Response.Status.FORBIDDEN).build();
}
}
private void authenticate(String username, String password) throws Exception {
// Authenticate against a database, LDAP, file or whatever
// Throw an Exception if the credentials are invalid
}
private String issueToken(String username) {
// Issue a token (can be a random String persisted to a database or a JWT token)
// The issued token must be associated to a user
// Return the issued token
}
}
시 가 " " " " "인 입니다.403
(서양속담, 친구속담)
검증되면 credential을 가진 .200
(OK)가 반환되고 발행된 토큰이 응답 페이로드로 클라이언트에 전송됩니다.클라이언트는 모든 요청에서 토큰을 서버로 전송해야 합니다.
비시를 할 때application/x-www-form-urlencoded
payload의 predential.
username=admin&password=123456
폼 파라미터 대신 사용자 이름과 비밀번호를 클래스로 랩할 수 있습니다.
public class Credentials implements Serializable {
private String username;
private String password;
// Getters and setters omitted
}
다음으로 JSON으로 소비합니다.
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response authenticateUser(Credentials credentials) {
String username = credentials.getUsername();
String password = credentials.getPassword();
// Authenticate the user, issue a token and return a response
}
이 방법을 사용하면 클라이언트는 요구의 페이로드에 다음 형식으로 credential을 전송해야 합니다.
{
"username": "admin",
"password": "123456"
}
요청에서 토큰 추출 및 검증
.Authorization
를 들어 과 같습니다예를 들어 다음과 같습니다.
Authorization: Bearer <token-goes-here>
표준 HTTP 헤더의 이름은 허가가 아닌 인증 정보를 전송하기 때문에 유감입니다.단, 서버에 자격 증명을 보내기 위한 표준 HTTP 헤더입니다.
JAX-RS는 필터 및 인터셉터를 리소스 클래스 및 메서드에 바인딩하기 위해 다른 주석을 작성하기 위해 사용되는 메타 주석을 제공합니다.의 정의@Secured
★★★★
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured { }
위에서 정의한 이름 바인딩 주석은 필터 클래스를 꾸미기 위해 사용됩니다. 필터 클래스는 리소스 메서드에 의해 처리되기 전에 요청을 대행 수신할 수 있습니다.를 사용하여 HTTP 요청 헤더에 액세스하여 토큰을 추출할 수 있습니다.
@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
private static final String REALM = "example";
private static final String AUTHENTICATION_SCHEME = "Bearer";
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the Authorization header from the request
String authorizationHeader =
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
// Validate the Authorization header
if (!isTokenBasedAuthentication(authorizationHeader)) {
abortWithUnauthorized(requestContext);
return;
}
// Extract the token from the Authorization header
String token = authorizationHeader
.substring(AUTHENTICATION_SCHEME.length()).trim();
try {
// Validate the token
validateToken(token);
} catch (Exception e) {
abortWithUnauthorized(requestContext);
}
}
private boolean isTokenBasedAuthentication(String authorizationHeader) {
// Check if the Authorization header is valid
// It must not be null and must be prefixed with "Bearer" plus a whitespace
// The authentication scheme comparison must be case-insensitive
return authorizationHeader != null && authorizationHeader.toLowerCase()
.startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " ");
}
private void abortWithUnauthorized(ContainerRequestContext requestContext) {
// Abort the filter chain with a 401 status code response
// The WWW-Authenticate header is sent along with the response
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE,
AUTHENTICATION_SCHEME + " realm=\"" + REALM + "\"")
.build());
}
private void validateToken(String token) throws Exception {
// Check if the token was issued by the server and if it's not expired
// Throw an Exception if the token is invalid
}
}
가 "" " " " " 의 입니다.401
(무허가)가 반환됩니다.그렇지 않으면 요청은 리소스 메서드로 진행됩니다.
REST 엔드포인트 보호
클래스에 , 「」로 .@Secured
주석 작성.주석이 달린 메서드 및/또는 클래스에 대해 필터가 실행됩니다.즉, 요청이 유효한 토큰을 사용하여 수행된 경우에만 이러한 엔드포인트에 도달합니다.
인증이 필요 없는 메서드 또는 클래스가 있는 경우 주석을 달지 마십시오.
@Path("/example")
public class ExampleResource {
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myUnsecuredMethod(@PathParam("id") Long id) {
// This method is not annotated with @Secured
// The authentication filter won't be executed before invoking this method
...
}
@DELETE
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response mySecuredMethod(@PathParam("id") Long id) {
// This method is annotated with @Secured
// The authentication filter will be executed before invoking this method
// The HTTP request must be performed with a valid token
...
}
}
위의 예에서 필터는 다음 명령어에 대해서만 실행됩니다.mySecuredMethod(Long)
방법에는@Secured
.
현재 사용자 식별
REST API를 다시 요청하고 있는 사용자를 알아야 합니다.이를 달성하기 위해 다음과 같은 방법을 사용할 수 있습니다.
현재 요청의 보안 컨텍스트 재정의
메서드 내에서 현재 요청에 대해 새 인스턴스를 설정할 수 있습니다.다음으로 를 덮어쓰고 인스턴스를 반환합니다.
final SecurityContext currentSecurityContext = requestContext.getSecurityContext();
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return () -> username;
}
@Override
public boolean isUserInRole(String role) {
return true;
}
@Override
public boolean isSecure() {
return currentSecurityContext.isSecure();
}
@Override
public String getAuthenticationScheme() {
return AUTHENTICATION_SCHEME;
}
});
토큰을 사용하여 사용자 ID(사용자 이름)를 검색합니다.이 ID는 의 이름이 됩니다.
임의의 JAX-RS 자원 클래스에 를 삽입합니다.
@Context
SecurityContext securityContext;
JAX-RS 리소스 방식에서도 동일한 작업을 수행할 수 있습니다.
@GET
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id,
@Context SecurityContext securityContext) {
...
}
그리고 다음 정보를 얻습니다.
Principal principal = securityContext.getUserPrincipal();
String username = principal.getName();
CDI(Context and Dependency Injection) 사용
어떤 이유로 덮어쓰지 않을 경우 이벤트나 제작자 등의 유용한 기능을 제공하는 CDI(Context and Dependency Injection)를 사용할 수 있습니다.
CDI 한정자를 만듭니다.
@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser { }
고객님의 고객명AuthenticationFilter
위에서 작성한 주석 삽입@AuthenticatedUser
:
@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;
인증이 성공하면 사용자 이름을 파라미터로 전달하는 이벤트를 실행합니다(이 토큰은 사용자에 대해 발행되며 토큰은 사용자 ID 검색에 사용됩니다).
userAuthenticatedEvent.fire(username);
응용 프로그램에 사용자를 나타내는 클래스가 있을 수 있습니다.이 반을 을을부라고 .User
.
이벤트를 CDI bean을 .User
하는 사용자 을 할당하고, 을 「」에 합니다.authenticatedUser
다음 중 하나:
@RequestScoped
public class AuthenticatedUserProducer {
@Produces
@RequestScoped
@AuthenticatedUser
private User authenticatedUser;
public void handleAuthenticationEvent(@Observes @AuthenticatedUser String username) {
this.authenticatedUser = findUser(username);
}
private User findUser(String username) {
// Hit the the database or a service to find a user by its username and return it
// Return the User instance
}
}
authenticatedUser
[ ]에 의해, 「 」가 됩니다.User
컨테이너 관리 콩(JAX-RS 서비스, CDI 콩, 서블릿 및 EJB 등)에 주입할 수 있는 인스턴스입니다.하여 '먹다'를 합니다.User
CDI:
@Inject
@AuthenticatedUser
User authenticatedUser;
CDI 주석은 JAX-RS 주석과 다릅니다.
- CDI:
javax.enterprise.inject.Produces
- JAX-RS:
javax.ws.rs.Produces
CDI 주석을 사용하는 것을 확인합니다.AuthenticatedUserProducer
bean.bean.contract.
여기서 중요한 것은 필터와 콩 사이에서 데이터를 공유할 수 있도록 주석이 달린 콩입니다.이벤트를 사용하지 않으려면 필터를 수정하여 인증된 사용자를 요청 범위 빈에 저장한 후 JAX-RS 리소스 클래스에서 읽을 수 있습니다.
를 덮어쓰는 어프로치와 달리 CDI 어프로치를 사용하면 JAX-RS 자원 및 공급자 이외의 콩에서 인증된 사용자를 취득할 수 있습니다.
역할 기반 인증 지원
역할 기반 인증을 지원하는 방법에 대한 자세한 내용은 다른 답변을 참조하십시오.
토큰 발행
토큰은 다음과 같습니다.
- 불투명: 값 자체(임의 문자열 등) 이외의 세부 정보를 표시하지 않습니다.
- 내장형:토큰 자체에 대한 상세 정보가 포함되어 있습니다(JWT 등).
자세한 내용은 아래를 참조하십시오.
토큰으로서의 랜덤 문자열
토큰은 랜덤 문자열을 생성하여 사용자 식별자 및 유효기간과 함께 데이터베이스에 유지함으로써 발행할 수 있습니다.Java에서 랜덤 문자열을 생성하는 방법의 좋은 예를 여기에서 볼 수 있습니다.다음 항목도 사용할 수 있습니다.
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT(JSON Web 토큰)
JWT(JSON Web Token)는 두 당사자 간의 클레임을 안전하게 표현하기 위한 표준 방법으로 RFC 7519에 정의되어 있습니다.
이 토큰은 독립형 토큰으로 클레임에 세부 정보를 저장할 수 있습니다.이러한 클레임은 Base64로 부호화된 JSON인 토큰 페이로드에 저장됩니다.다음으로 RFC 7519에 등록된 클레임과 그 의미를 나타냅니다(자세한 내용은 RFC 전문을 참조하십시오).
iss
: 토큰을 발행한 주체.sub
: JWT의 대상이 되는 주체.exp
: 토큰의 유효기간.- : 토큰이 처리를 위해 받아들여지기 시작하는 시간
nbf
. - : 토큰이 발행된 시각
iat
. - : 토큰의 고유 식별자
jti
.
암호와 같은 중요한 데이터를 토큰에 저장해서는 안 됩니다.
payload는 클라이언트에서 읽을 수 있으며 토큰의 무결성은 서버에서 서명을 확인함으로써 쉽게 확인할 수 있습니다.시그니처는 토큰의 조작을 방지하는 것입니다.
추적할 필요가 없는 경우에는 JWT 토큰을 유지할 필요가 없습니다.토큰을 유지함으로써 토큰의 액세스를 무효화하고 취소할 수 있습니다.JWT 토큰을 추적하려면 토큰 전체를 서버에 유지하는 대신 토큰을 발행한 사용자, 유효기간 등 기타 세부 정보와 함께 토큰 식별자(jti
클레임)를 유지할 수 있습니다.
토큰을 유지할 때는 데이터베이스가 무한히 커지는 것을 방지하기 위해 오래된 토큰을 삭제하는 것을 항상 고려해 주십시오.
JWT 사용
JWT 토큰을 발행하고 검증하는 Java 라이브러리는 다음과 같습니다.
JWT와 연계할 수 있는 기타 유용한 리소스를 찾으려면 http://jwt.io을 참조하십시오.
JWT를 사용한 토큰 취소 처리
토큰을 해지하려면 토큰을 추적해야 합니다.서버 측에 전체 토큰을 저장할 필요가 없으며 토큰 식별자(일치해야 함) 및 필요한 경우 일부 메타데이터만 저장할 필요가 있습니다.토큰 ID에는 UUID를 사용할 수 있습니다.
클레임을 사용하여 토큰 식별자를 토큰에 저장해야 합니다.토큰을 검증할 때 서버 측에 있는 토큰 식별자에 대해 클레임 값을 확인하여 토큰이 취소되지 않았는지 확인하십시오.
보안을 위해 사용자가 암호를 변경할 때 사용자의 모든 토큰을 해지하십시오.
추가 정보
- 어떤 종류의 인증을 사용할지는 중요하지 않습니다.man-in-the-middle 공격을 방지하기 위해 항상 HTTPS 접속 상단에서 실행합니다.
- 토큰에 대한 자세한 내용은 Information Security에서 제공하는 이 질문을 참조하십시오.
- 이 문서에서는 토큰 기반 인증에 대한 유용한 정보를 제공합니다.
이 답변은 인증에 관한 답변으로, 인증에 관한 이전 답변의 보완입니다.
왜 또 다른 대답을 하죠?JSR-250 주석을 지원하는 방법에 대한 자세한 내용을 추가하여 이전 답변을 확장하려고 했습니다.그러나 원래의 답은 너무 길어졌고 최대 길이인 30,000자를 초과했다.그래서 인증 세부사항을 모두 이 답변으로 옮겼습니다.다른 답변은 인증과 토큰 발행에 중점을 두고 있습니다.
@Secured
다른 응답에 나타난 인증 흐름 외에 REST 엔드 포인트에서는 롤 기반 인가를 지원할 수 있습니다.
열거를 작성하고 필요에 따라 역할을 정의합니다.
public enum Role {
ROLE_1,
ROLE_2,
ROLE_3
}
변경합니다.@Secured
역할을 지원하기 위해 이전에 만든 이름 바인딩 주석:
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured {
Role[] value() default {};
}
다음 자원 을 달아주세요.@Secured
인가를 실행합니다.메서드 주석이 클래스 주석을 재정의합니다.
@Path("/example")
@Secured({Role.ROLE_1})
public class ExampleResource {
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id) {
// This method is not annotated with @Secured
// But it's declared within a class annotated with @Secured({Role.ROLE_1})
// So it only can be executed by the users who have the ROLE_1 role
...
}
@DELETE
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
@Secured({Role.ROLE_1, Role.ROLE_2})
public Response myOtherMethod(@PathParam("id") Long id) {
// This method is annotated with @Secured({Role.ROLE_1, Role.ROLE_2})
// The method annotation overrides the class annotation
// So it only can be executed by the users who have the ROLE_1 or ROLE_2 roles
...
}
}
priority를 가진 필터를 만듭니다.이 필터는 이전에 정의된 priority 필터 후에 실행됩니다.
를 사용하여 요구를 처리할 리소스와 리소스를 취득한 후@Secured
★★★★
@Secured
@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {
@Context
private ResourceInfo resourceInfo;
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the resource class which matches with the requested URL
// Extract the roles declared by it
Class<?> resourceClass = resourceInfo.getResourceClass();
List<Role> classRoles = extractRoles(resourceClass);
// Get the resource method which matches with the requested URL
// Extract the roles declared by it
Method resourceMethod = resourceInfo.getResourceMethod();
List<Role> methodRoles = extractRoles(resourceMethod);
try {
// Check if the user is allowed to execute the method
// The method annotations override the class annotations
if (methodRoles.isEmpty()) {
checkPermissions(classRoles);
} else {
checkPermissions(methodRoles);
}
} catch (Exception e) {
requestContext.abortWith(
Response.status(Response.Status.FORBIDDEN).build());
}
}
// Extract the roles from the annotated element
private List<Role> extractRoles(AnnotatedElement annotatedElement) {
if (annotatedElement == null) {
return new ArrayList<Role>();
} else {
Secured secured = annotatedElement.getAnnotation(Secured.class);
if (secured == null) {
return new ArrayList<Role>();
} else {
Role[] allowedRoles = secured.value();
return Arrays.asList(allowedRoles);
}
}
}
private void checkPermissions(List<Role> allowedRoles) throws Exception {
// Check if the user contains one of the allowed roles
// Throw an Exception if the user has not permission to execute the method
}
}
은 """로 됩니다"""는 ""로 됩니다.403
(신호)
요청을 수행하는 사용자를 확인하려면 이전 답변을 참조하십시오.사용하는 어프로치에 따라서, 에서 취득할 수도, CDI 를 사용해 주입할 수도 있습니다.
「」의 '@Secured
이치노 인증된 모든 사용자가 해당 엔드포인트에 액세스할 수 있으며 사용자의 역할은 무시한다고 가정할 수 있습니다.
JSR-250 어노테이션에 의한 롤 베이스
의 롤을 도 있습니다.@Secured
상기와 같이 주석, 및 등의 JSR-250 주석을 고려할 수 있습니다.
JAX-RS는 이러한 주석을 즉시 사용할 수 없지만 필터를 통해 이를 달성할 수 있습니다.이 모든 것을 서포트하는 경우는, 다음의 점에 주의해 주세요.
@DenyAll
메서드에서는 클래스보다 우선됩니다.@RolesAllowed
method가 클래스보다 우선합니다.@PermitAll
method가 클래스보다 우선합니다.@DenyAll
수업에 연결할 수 없습니다.@RolesAllowed
클래스보다 우선합니다.
따라서 JSR-250 주석을 체크하는 인가 필터는 다음과 같습니다.
@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {
@Context
private ResourceInfo resourceInfo;
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
Method method = resourceInfo.getResourceMethod();
// @DenyAll on the method takes precedence over @RolesAllowed and @PermitAll
if (method.isAnnotationPresent(DenyAll.class)) {
refuseRequest();
}
// @RolesAllowed on the method takes precedence over @PermitAll
RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class);
if (rolesAllowed != null) {
performAuthorization(rolesAllowed.value(), requestContext);
return;
}
// @PermitAll on the method takes precedence over @RolesAllowed on the class
if (method.isAnnotationPresent(PermitAll.class)) {
// Do nothing
return;
}
// @DenyAll can't be attached to classes
// @RolesAllowed on the class takes precedence over @PermitAll on the class
rolesAllowed =
resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class);
if (rolesAllowed != null) {
performAuthorization(rolesAllowed.value(), requestContext);
}
// @PermitAll on the class
if (resourceInfo.getResourceClass().isAnnotationPresent(PermitAll.class)) {
// Do nothing
return;
}
// Authentication is required for non-annotated methods
if (!isAuthenticated(requestContext)) {
refuseRequest();
}
}
/**
* Perform authorization based on roles.
*
* @param rolesAllowed
* @param requestContext
*/
private void performAuthorization(String[] rolesAllowed,
ContainerRequestContext requestContext) {
if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
refuseRequest();
}
for (final String role : rolesAllowed) {
if (requestContext.getSecurityContext().isUserInRole(role)) {
return;
}
}
refuseRequest();
}
/**
* Check if the user is authenticated.
*
* @param requestContext
* @return
*/
private boolean isAuthenticated(final ContainerRequestContext requestContext) {
// Return true if the user is authenticated or false otherwise
// An implementation could be like:
// return requestContext.getSecurityContext().getUserPrincipal() != null;
}
/**
* Refuse the request.
*/
private void refuseRequest() {
throw new AccessDeniedException(
"You don't have permissions to perform this action.");
}
}
주의: 상기 실장은 Jersey에 근거하고 있습니다.Jersey를 사용하는 경우 필터를 직접 작성할 필요가 없습니다.기존의 실장만 사용하면 됩니다.
언급URL : https://stackoverflow.com/questions/26777083/how-to-implement-rest-token-based-authentication-with-jax-rs-and-jersey
'programing' 카테고리의 다른 글
Vue JS: 데이터 반환 내부의 값을 업데이트하는 방법 (0) | 2022.07.13 |
---|---|
Python: * 및 **가 / 및 sqrt()보다 빠른 이유는 무엇입니까? (0) | 2022.07.13 |
C 함수는 왜 네임 매니지먼트가 안 되나요? (0) | 2022.07.13 |
Vue Router가 루트로 이동하지만 잘못된 컴포넌트를 로드함 (0) | 2022.07.13 |
노드용 파일을 포함합니다.JS(vue) (0) | 2022.07.13 |