android - 如何使用 Spring Boot Server 无限期地保持 Android 应用程序登录?
问题描述
好吧,老实说,这个标题有点误导,但我想不出更好的标题。
我有一个基于 Spring Boot 的服务器和一个 Android 应用程序。用户使用用户名和密码进行身份验证,然后提供 JWT 身份验证令牌,然后在后续请求中使用该令牌来访问 API。令牌有效期为一小时,之后用户使用其凭据再次登录,这对用户来说很不方便。事实上,只有当用户明确退出应用程序时,我才需要应用程序再次请求凭据。
我认为使用刷新令牌是解决此问题的方法,但我不确定如何在我当前的代码中实现它?
AuthController 的认证方法
@PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsernameOrEmail(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
}
JwtAuthenticationResponse
public class JwtAuthenticationResponse {
private String accessToken;
private String refreshToken = "Blank";
private String tokenType = "Bearer";
//Getters and Setters
}
JwtTokenProvider
@Component
public class JwtTokenProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationInMs}")
private int jwtExpirationInMs;
public String generateToken(Authentication authentication) {
UserPrincipal userDetails = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(Long.toString(userDetails.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty.");
}
return false;
}
}
我知道我需要另一种方法来在JwtTokenProvider中创建 JWT 刷新令牌,我可以从AuthController中的authenticateUser方法调用它。该刷新令牌将在accessToken过期后由 Android 应用程序发回以获取新的accessToken。我怎么做?
解决方案
您可以在生成访问令牌时创建刷新令牌并针对该用户在数据库中持久保存。以下是您需要做的代码更改 -
JwtRefreshToken 模型
创建一个 JwtRefreshToken 域模型:
package com.example.polls.model;
import javax.persistence.*;
import java.time.Instant;
@Entity
@Table(name = "refresh_tokens")
public class JwtRefreshToken {
@Id
private String token;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
private Instant expirationDateTime;
public JwtRefreshToken() {
}
public JwtRefreshToken(String token) {
this.token = token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Instant getExpirationDateTime() {
return expirationDateTime;
}
public void setExpirationDateTime(Instant expirationDateTime) {
this.expirationDateTime = expirationDateTime;
}
}
JwtRefreshToken 存储库
创建用于访问 RefreshToken 的存储库:
package com.example.polls.repository;
import com.example.polls.model.JwtRefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface JwtRefreshTokenRepository extends JpaRepository<JwtRefreshToken, String> {
}
JwtAuthenticationResponse
在 Authentication 响应中添加 refreshToken 和 expiresInMsec 字段:
package com.example.polls.payload;
/**
* Created by rajeevkumarsingh on 19/08/17.
*/
public class JwtAuthenticationResponse {
private String accessToken;
private String refreshToken;
private String tokenType = "Bearer";
private Long expiresInMsec;
public JwtAuthenticationResponse(String accessToken, String refreshToken, Long expiresInMsec) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expiresInMsec = expiresInMsec;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public Long getExpiresInMsec() {
return expiresInMsec;
}
public void setExpiresInMsec(Long expiresInMsec) {
this.expiresInMsec = expiresInMsec;
}
}
JwtTokenProvider
添加方法以生成刷新令牌:
package com.example.polls.security;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.UUID;
@Component
public class JwtTokenProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationInMs}")
private long jwtExpirationInMs;
public String generateToken(UserPrincipal userPrincipal) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String generateRefreshToken() {
// generate a random UUID as refresh token
return UUID.randomUUID().toString();
}
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty.");
}
return false;
}
}
授权控制器
持久化刷新令牌并编写 API 以刷新访问令牌:
package com.example.polls.controller;
import com.example.polls.exception.AppException;
import com.example.polls.exception.BadRequestException;
import com.example.polls.model.JwtRefreshToken;
import com.example.polls.model.Role;
import com.example.polls.model.RoleName;
import com.example.polls.model.User;
import com.example.polls.payload.*;
import com.example.polls.repository.JwtRefreshTokenRepository;
import com.example.polls.repository.RoleRepository;
import com.example.polls.repository.UserRepository;
import com.example.polls.security.JwtTokenProvider;
import com.example.polls.security.UserPrincipal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.validation.Valid;
import java.net.URI;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
/**
* Created by rajeevkumarsingh on 02/08/17.
*/
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
JwtTokenProvider tokenProvider;
@Autowired
JwtRefreshTokenRepository jwtRefreshTokenRepository;
@Value("${app.jwtExpirationInMs}")
private long jwtExpirationInMs;
@PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsernameOrEmail(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
String accessToken = tokenProvider.generateToken(userPrincipal);
String refreshToken = tokenProvider.generateRefreshToken();
saveRefreshToken(userPrincipal, refreshToken);
return ResponseEntity.ok(new JwtAuthenticationResponse(accessToken, refreshToken, jwtExpirationInMs));
}
@PostMapping("/refreshToken")
public ResponseEntity<?> refreshAccessToken(@Valid @RequestBody RefreshTokenRequest refreshTokenRequest) {
return jwtRefreshTokenRepository.findById(refreshTokenRequest.getRefreshToken()).map(jwtRefreshToken -> {
User user = jwtRefreshToken.getUser();
String accessToken = tokenProvider.generateToken(UserPrincipal.create(user));
return ResponseEntity.ok(new JwtAuthenticationResponse(accessToken, jwtRefreshToken.getToken(), jwtExpirationInMs));
}).orElseThrow(() -> new BadRequestException("Invalid Refresh Token"));
}
private void saveRefreshToken(UserPrincipal userPrincipal, String refreshToken) {
// Persist Refresh Token
JwtRefreshToken jwtRefreshToken = new JwtRefreshToken(refreshToken);
jwtRefreshToken.setUser(userRepository.getOne(userPrincipal.getId()));
Instant expirationDateTime = Instant.now().plus(360, ChronoUnit.DAYS); // Todo Add this in application.properties
jwtRefreshToken.setExpirationDateTime(expirationDateTime);
jwtRefreshTokenRepository.save(jwtRefreshToken);
}
@PostMapping("/signup")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
if(userRepository.existsByUsername(signUpRequest.getUsername())) {
return new ResponseEntity(new ApiResponse(false, "Username is already taken!"),
HttpStatus.BAD_REQUEST);
}
if(userRepository.existsByEmail(signUpRequest.getEmail())) {
return new ResponseEntity(new ApiResponse(false, "Email Address already in use!"),
HttpStatus.BAD_REQUEST);
}
// Creating user's account
User user = new User(signUpRequest.getName(), signUpRequest.getUsername(),
signUpRequest.getEmail(), signUpRequest.getPassword());
user.setPassword(passwordEncoder.encode(user.getPassword()));
Role userRole = roleRepository.findByName(RoleName.ROLE_USER)
.orElseThrow(() -> new AppException("User Role not set."));
user.setRoles(Collections.singleton(userRole));
User result = userRepository.save(user);
URI location = ServletUriComponentsBuilder
.fromCurrentContextPath().path("/users/{username}")
.buildAndExpand(result.getUsername()).toUri();
return ResponseEntity.created(location).body(new ApiResponse(true, "User registered successfully"));
}
}
刷新令牌请求
这是由 /api/auth/refreshToken API 使用的。
package com.example.polls.payload;
import javax.validation.constraints.NotBlank;
public class RefreshTokenRequest {
@NotBlank
private String refreshToken;
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
}
推荐阅读
- php - 通过 html 表单输入的数据未存储在 phpmyadmin 的数据库中
- c++ - 铿锵虫?使用指针作为模板参数
- python - 为什么在我训练我的 RNN 网络以解决分类问题后输出总是相同的?
- spring - 微服务的多个实例是什么意思?
- bash - 尝试合并 2 个文件但忽略新行
- javascript - 如何从 TypeScript 中的 3rd 方库重新导出类型?
- php - macOS 和 Linux 上使用内置 Web 服务器的 PHP CLI 是否像在 Windows 上一样执行命令?
- postgresql - 如何锁定多工人速率限制的行?
- c - 我正在尝试使用 COM Moniker 抑制 UAC 提示
- python - SymPy 用符号代替向量