SpringSecurity JWT验证

原版的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,在这两部分做一些拓展就行了。

到这里就已经结束了,如有错误欢迎指出。

未经允许禁止转载本站内容,经允许转载后请严格遵守CC-BY-NC-ND知识共享协议4.0,代码部分则采用GPL v3.0协议
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇