Spring Security Remember me是一种通过在用户登录后为用户生成Token,使用户在下一次访问时可以跳过登录,直接使用Token进行自动登录的机制。
实现Remember me功能可以使用Spring Security提供的RememberMeAuthenticationFilter过滤器,该过滤器会在用户登录成功后创建一个Token,将Token信息保存在客户端cookie和服务器端的数据库中,并在下次登录时通过Token自动登录。
下面我们详细讲解Remember me的使用及原理:
设置Remember Me
要启用Remember Me,需要在Security配置文件中启用Remember Me功能,并指定一些参数。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ... 其他配置 ...
.rememberMe()
.key("uniqueAndSecret")
.userDetailsService(userDetailsService)
.tokenValiditySeconds(86400) //有效期1天
;
}
// ... 其他配置 ...
}
上面的代码添加了以下Remember Me的配置:
- key("uniqueAndSecret"):指定生成Token时使用的Key,用于加密Token信息;
- userDetailsService(userDetailsService):指定用户验证时使用的UserDetailsService;
- tokenValiditySeconds(86400):指定Token的有效期,该例中为1天。
实现UserDetailsService
UserDetailsService是提供用户信息的一个接口,Spring Security需要通过该接口获取用户的信息。我们需要根据实际情况创建一个对象并实现该接口,并在配置中指定该对象。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
}
return createUserDetails(user);
}
private UserDetails createUserDetails(User user) {
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
for (Role role : user.getRoles()) {
grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
grantedAuthorities
);
}
}
在上面的代码中,我们使用UserRepository从数据库中获取并返回UserDetails,其中UserDetails包括用户名、密码和用户具有的权限等信息。
如果要使用Remember Me,还需要将UserDetails的密码与Token结合使用,以验证Token的有效性。
自定义Remember Me Cookie
Remember Me标准设置会在客户端生成一个名为remember-me的cookie,如果需要自定义cookie名或其他属性,可以使用rememberMeCookie()方法。
http
// ... 其他配置 ...
.rememberMe()
.key("uniqueAndSecret")
.userDetailsService(userDetailsService)
.tokenValiditySeconds(86400) //有效期1天
.rememberMeCookieName("my-remember-me") //自定义cookie名称
;
实现自定义Remember Me Token
如果需要自定义Remember Me Token,可以实现PersistentTokenRepository接口,以便将Token保存在持久化存储中。
1.自定义Token Entity
首先,需要创建一个Token Entity,包含以下字段:
- String series:Token的series;
- String username:与Token相关联的用户名;
- String tokenValue:Token的值;
- Date date:Token创建时间。
@Entity
@Table(name = "persistent_logins")
public class PersistentLogin {
@Id
private String series;
private String username;
@Column(name="token", unique=true, nullable=false)
private String tokenValue;
@Temporal(TemporalType.TIMESTAMP)
private Date last_used;
// ... getter and setter ...
}
2.实现PersistentTokenRepository接口
然后,需要实现PersistentTokenRepository接口,该接口提供了向持久化存储中添加和读取Token的方法。
@Repository
public class PersistentTokenRepositoryImpl implements PersistentTokenRepository {
@Autowired
private PersistentLoginRepository persistentLoginRepository;
@Override
public void createNewToken(PersistentRememberMeToken token) {
PersistentLogin login = new PersistentLogin();
login.setSeries(token.getSeries());
login.setUsername(token.getUsername());
login.setTokenValue(token.getTokenValue());
login.setLast_used(token.getDate());
persistentLoginRepository.save(login);
}
@Override
public void updateToken(String series, String tokenValue, Date lastUsed) {
PersistentLogin login = persistentLoginRepository.findById(series).orElse(null);
if (login != null) {
login.setTokenValue(tokenValue);
login.setLast_used(lastUsed);
persistentLoginRepository.save(login);
}
}
@Override
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
PersistentLogin login = persistentLoginRepository.findById(seriesId).orElse(null);
if (login == null) {
return null;
}
return new PersistentRememberMeToken(
login.getUsername(),
login.getSeries(),
login.getTokenValue(),
login.getLast_used()
);
}
@Override
public void removeUserTokens(String username) {
persistentLoginRepository.deleteAllByUsername(username);
}
}
在上面的实现中,我们使用了PersistentLoginRepository将Token保存在数据库中。我们使用了Spring Data JPA简化了数据库操作,PersistentLoginRepository中提供了save()和findById()等方法。
3.启用自定义Remember Me Token
使用以上自定义的Token实现时,需要在配置中指定Remember Me使用自定义的Token实现。
http
// ... 其他配置 ...
.rememberMe()
.key("uniqueAndSecret")
.userDetailsService(userDetailsService)
.tokenValiditySeconds(86400) //有效期1天
.rememberMeCookieName("my-remember-me") //自定义cookie名称
.tokenRepository(tokenRepository()) //自定义Token实现
;
@Bean
public PersistentTokenRepository tokenRepository() {
return new PersistentTokenRepositoryImpl();
}
示例一:使用Remember Me功能
在上面的代码配置完成后,用户登录成功后,记住我功能会在客户端生成一个名为remember-me的cookie。再次访问系统时,如果客户端有该cookie,系统将自动登录。
以下是示例代码:
@Controller
@RequestMapping("/")
public class UserController {
@GetMapping("/home")
public String home() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
User user = userService.getUserByUsername(username);
return "Hello, " + user.getUsername() + "!";
}
@GetMapping("/login")
public String showLoginPage(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
return "redirect:/";
}
return "login";
}
@PostMapping("/login")
public String login(
@RequestParam String username,
@RequestParam String password,
@RequestParam(required = false) boolean rememberMe,
HttpServletRequest request,
HttpServletResponse response
) {
UsernamePasswordAuthenticationToken authReq =
new UsernamePasswordAuthenticationToken(username, password);
if (rememberMe) {
authReq.setDetails(new PersistentRememberMeToken("uniqueAndSecret", userDetailsService().loadUserByUsername(username), new SecureRandom().generateSeed(25)));
}
Authentication auth = authenticationManager.authenticate(authReq);
if (auth.isAuthenticated()) {
SecurityContextHolder.getContext().setAuthentication(auth);
return "redirect:/";
} else {
return "login";
}
}
}
在上述代码中,我们使用了rememberMe参数来标志是否使用Remember Me功能,并在登录时向Authentication中添加了一个PersistentRememberMeToken。
示例二:自定义Remember Me页面
在默认情况下,如果Remember Me功能启用,在需要重新登录的时候,系统会自动跳转到默认的Remember Me页面。如果需要自定义该页面,可以使用loginPage()方法指定自定义页面的路径。
以下是示例配置:
http
// ... 其他配置 ...
.rememberMe()
.key("uniqueAndSecret")
.userDetailsService(userDetailsService)
.tokenValiditySeconds(86400) //有效期1天
.rememberMeCookieName("my-remember-me") //自定义cookie名称
.tokenRepository(tokenRepository()) //自定义Token实现
.rememberMeParameter("rememberMe") //自定义Remember Me的请求参数名
.tokenValiditySeconds(3600) //自定义Token有效期
.useSecureCookie(true) //启用HTTPS,防止cookie被窃取
.authenticationSuccessHandler(successHandler()) //登录成功后执行的处理
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.failureUrl("/login?error")
.usernameParameter("username")
.passwordParameter("password")
;
在指定了登录页面之后,在处理登录失败请求时需要返回正确的视图。示例代码如下:
@Controller
@RequestMapping("/")
public class UserController {
// ...
@PostMapping("/login")
public String login(
@RequestParam String username,
@RequestParam String password,
@RequestParam(required = false) boolean rememberMe,
HttpServletRequest request,
HttpServletResponse response
) throws IOException {
UsernamePasswordAuthenticationToken authReq =
new UsernamePasswordAuthenticationToken(username, password);
if (rememberMe) {
authReq.setDetails(new PersistentRememberMeToken("uniqueAndSecret", userDetailsService().loadUserByUsername(username), new SecureRandom().generateSeed(25)));
}
Authentication auth = authenticationManager.authenticate(authReq);
if (auth.isAuthenticated()) {
SecurityContextHolder.getContext().setAuthentication(auth);
return "redirect:/";
} else {
response.sendRedirect("/login?error");
return null;
}
}
@GetMapping("/login")
public String showLoginPage(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
return "redirect:/";
}
return "login";
}
@GetMapping("/login/remember-me")
public String showRememberMePage() {
return "rememberMe";
}
// ...
}
在login()方法中,如果登录失败,可以使用response.sendRedirect()方法返回OringinURI,即原始请求地址,以便在登录页面中显示错误信息。
在上述代码中,我们自定义了/login/remember-me页面,并使用了rememberMeParameter()方法配置了自定义的Remember Me请求参数名。同时,我们还启用了HTTPS以防止cookie被窃取,在登录成功后使用了authenticationSuccessHandler()方法处理登录成功后的逻辑。
以上就是Spring Security Remember me使用及原理详解的完整攻略。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Spring Security Remember me使用及原理详解 - Python技术站