本文最后更新于 89 天前,其中的信息可能已经有所发展或是发生改变。
原版的SpringSecurity是根据Session来记录用户的,已经登录的用户可以通过同一个Session来取得访问权限。但是这个方法不适用于前后端分离的项目,现在对它进行改造。
基本思路是先禁用SpringSecurity的Session,然后自定义用户登录类与过滤链。
当用户登录时颁发一个JWT令牌,当用户请求接口时通过一个自定义的过滤链来检查用户的Token是否有效。
基本的依赖导入就先略过了,下面直接给代码。
准备
用户类
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User implements UserDetails {
private Long id;
private String username;
private String password;
private String email;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return new ArrayList<>();
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
}
用户服务类
public interface UserService extends UserDetailsService {
}
@Service
public class UserServiceImpl implements UserService {
// Test data
private static Map<String, User> mapOfUsers = new HashMap<>();
static {
mapOfUsers.put("admin", new User(1L, "admin", "admin", ""));
mapOfUsers.put("user1", new User(2L, "user1", "user1", ""));
mapOfUsers.put("user2", new User(3L, "user2", "user2", ""));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = mapOfUsers.get(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return user;
}
}
配置类
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(
registry -> registry.requestMatchers("/auth/login").permitAll()
.anyRequest().authenticated());
httpSecurity.sessionManagement(it -> it.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
httpSecurity.formLogin(AbstractHttpConfigurer::disable);
httpSecurity.csrf(AbstractHttpConfigurer::disable);
httpSecurity.exceptionHandling(it -> it.authenticationEntryPoint((request, response, e) -> {
response.setContentType("text/json;charset=utf-8");
if (e instanceof InsufficientAuthenticationException) {
response.getWriter().write("Token is invalid");
} else {
response.getWriter().write("Access denied");
}
}));
return httpSecurity.build();
}
/**
* Do not use NoOpPasswordEncoder in product env
* @return NoOpPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
用户登录
接下来我们要自定义用户登录的方法,当用户登录时,给用户颁发一个自定义的JWT-Token。
登录服务
public interface LoginService {
String login(String username, String password) throws Exception;
void logout(String username);
}
@Slf4j
@Service
public class LoginServiceImpl implements LoginService {
@Resource
private AuthenticationConfiguration configuration;
// UserId(String), Token
public static Map<String, String> loggedUserMap = new HashMap<>();
@Override
public String login(String username, String password) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
Authentication authenticate = null;
try {
authenticate = configuration.getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
} catch (Exception e) {
log.error(username + " login failed", e);
return null;
}
SecurityContextHolder.getContext().setAuthentication(authenticate);
User principal = (User) authenticate.getPrincipal();
// Build jwt token
long expires = 7 * 24 * 60 * 60 * 1000L;
String token = Jwts.builder()
.setSubject(authenticate.getName())
.signWith(SignatureAlgorithm.HS512, "ChangeToYours")
.setIssuer("Issuer")
.claim("uid", principal.getId())
.setExpiration(new Date(System.currentTimeMillis() + expires))
.compact();
// Save user token
loggedUserMap.put(principal.getId().toString(), token);
return token;
}
@Override
public void logout(String username) {
loggedUserMap.remove(username);
}
}
LoginController
@RestController
@RequestMapping("/auth")
public class LoginController {
@Resource
private LoginService loginService;
@PostMapping("/login")
public String login(String username, String password) {
String token = loginService.login(username, password);
return Objects.requireNonNullElse(token, "Login failed");
}
@PostMapping
public String logout(String username) {
loginService.logout(username);
return username;
}
}
SecurityFilter
接下来准备一个过滤链来验证用户请求接口时的Token。
@Slf4j
@Component
public class SecurityFilter extends GenericFilter {
@Resource
private UserService userService;
@Override
public void doFilter(
ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain
) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
if (request != null) {
String headerToken = request.getHeader("Authorization");
if (headerToken != null) {
String token = headerToken.replace("Bearer ", "");
if (!token.isEmpty()) {
if (validateToken(token)) {
Claims claims = Jwts.parser().setSigningKey("ChangeToYours")
.parseClaimsJws(token).getBody();
String uid = claims.get("uid").toString();
String storedToken = LoginServiceImpl.loggedUserMap.get(uid);
if (storedToken != null) {
User user = (User) userService.loadUserByUsername(claims.getSubject());
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else {
log.warn(uid + " auth failed");
}
}
}
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
private boolean validateToken(String token) {
try {
Claims claims = Jwts.parser().setSigningKey("ChangeToYours")
.parseClaimsJws(token).getBody();
return System.currentTimeMillis() < Long.parseLong(claims.get("exp").toString()) * 1000;
} catch (ExpiredJwtException e) {
// Token expired
return false;
} catch (SignatureException | MalformedJwtException e) {
return false;
}
}
}
这里套了很多层if,但是没必要,可以自己改造一下。
测试
测试类
先写一个测试的Controller:
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
public String sayHello() {
return "Hello, World!";
}
}
登录测试
`POST 127.0.0.1:8080/auth/login`
username: admin
password: admin
最终得到的token: `eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImlzcyI6Iklzc3VlciIsInVpZCI6MSwiZXhwIjoxNzE4NjgzNjc4fQ.wbXMSTLQDmjZyqb7OV1Ktfg1tu3ygTmuOKaUTGG5b60Aejk9HsfK4Os8TaBBCzVXjhcW3XUnH2dcySBPiQNT9A`
接口测试
`GET 127.0.0.1:8080/test/hello` 在Header中加上刚才得到的token,请求结果为 `Hello, World!` 就成功了。
总结
整个模型最重要的部分就是登录时生成Token和请求时验证Token,在这两部分做一些拓展就行了。
到这里就已经结束了,如有错误欢迎指出。