Spring with JWT auth,获取当前用户

Posted

技术标签:

【中文标题】Spring with JWT auth,获取当前用户【英文标题】:Spring with JWT auth, get current user 【发布时间】:2018-11-21 01:23:36 【问题描述】:

我有使用 JWT 令牌进行授权的 Spring Boot REST 应用程序。我想使用 @AuthenticationPrincipal 注释在控制器中获取当前登录的用户。但如果我从loadUserByUsername 返回自定义模型并且授权停止工作,它总是返回null。我的模型实现了UserDetails

我尝试扩展 org.springframework.security.core.userdetails.User,但我从 JWTAuthenticationFilter 中删除了默认构造函数不存在的错误 (ApplicationUser creds = new ObjectMapper().readValue(req.getInputStream(), ApplicationUser.class);)

怎么了?

UserDetailsS​​erviceImpl.java

@Service
public class UserDetailsServiceImpl implements UserDetailsService 
    private UserRepository userRepository;

    public UserDetailsServiceImpl(UserRepository userRepository) 
        this.userRepository = userRepository;
    

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
        ApplicationUser applicationUser = userRepository.findByUsername(username);
        if (applicationUser == null) throw new UsernameNotFoundException(username);

        return applicationUser;
    

ApplicationUser.java(模型)

@Entity
@Table(name = "users")
public class ApplicationUser implements UserDetails 

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(unique = true, nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

    public long getId() 
        return id;
    

    public void setId(long id) 
        this.id = id;
    

    public String getUsername() 
        return username;
    

    public void setUsername(String username) 
        this.username = username;
    

    public String getEmail() 
        return email;
    

    public void setEmail(String email) 
        this.email = email;
    

    public String getPassword() 
        return password;
    

    public void setPassword(String password) 
        this.password = password;
    

    @Override
    public boolean isAccountNonExpired() 
        return false;
    

    @Override
    public boolean isAccountNonLocked() 
        return false;
    

    @Override
    public boolean isCredentialsNonExpired() 
        return false;
    

    @Override
    public boolean isEnabled() 
        return false;
    

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() 
        return null;
    

JWTAuthenticationFilter

public class JWTAuthenticationFilter extends AbstractAuthenticationProcessingFilter 
    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) 
        super(new AntPathRequestMatcher(LOGIN_URL));

        this.authenticationManager = authenticationManager;
    

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req,
                                                HttpServletResponse res) throws AuthenticationException 
        try 
            ApplicationUser creds = new ObjectMapper()
                    .readValue(req.getInputStream(), ApplicationUser.class);

            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            creds.getUsername(),
                            creds.getPassword(),
                            new ArrayList<>())
            );
         catch (IOException e) 
            throw new RuntimeException(e);
        
    

    @Override
    protected void successfulAuthentication(HttpServletRequest req,
                                            HttpServletResponse res,
                                            FilterChain chain,
                                            Authentication auth) throws IOException, ServletException 

        String token = Jwts.builder()
                .setSubject(((ApplicationUser) auth.getPrincipal()).getUsername())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET.getBytes())
                .compact();

        res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
    

JWTAuthorizationFilter

public class JWTAuthorizationFilter extends BasicAuthenticationFilter 

    public JWTAuthorizationFilter(AuthenticationManager authManager) 
        super(authManager);
    

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws IOException, ServletException 
        String header = req.getHeader(HEADER_STRING);

        if (header == null || !header.startsWith(TOKEN_PREFIX)) 
            chain.doFilter(req, res);
            return;
        

        UsernamePasswordAuthenticationToken authentication = getAuthentication(req);

        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(req, res);
    

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) 
        String token = request.getHeader(HEADER_STRING);
        if (token != null) 
            // parse the token.
            String user;
            try 
                user = Jwts.parser()
                        .setSigningKey(SECRET.getBytes())
                        .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                        .getBody()
                        .getSubject();
             catch (SignatureException e) 
                return null;
            

            if (user != null) return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());

            return null;
        
        return null;
    

【问题讨论】:

【参考方案1】:

在您的情况下,@AuthenticationPrincipal 将返回一个带有用户名的字符串, 您可以通过调用控制器中的存储库并通过用户名获取用户或将存储库声明为@Bean 来获取用户并执行以下操作:

public class JWTAuthorizationFilter extends BasicAuthenticationFilter 

//Get the repository
private UserRepository userRepository;

public JWTAuthorizationFilter(AuthenticationManager authManager) 
    super(authManager);


@Override
protected void doFilterInternal(HttpServletRequest req,
                                HttpServletResponse res,
                                FilterChain chain) throws IOException, ServletException 
    String header = req.getHeader(HEADER_STRING);

    if (header == null || !header.startsWith(TOKEN_PREFIX)) 
        chain.doFilter(req, res);
        return;
    

    UsernamePasswordAuthenticationToken authentication = getAuthentication(req);

    SecurityContextHolder.getContext().setAuthentication(authentication);
    chain.doFilter(req, res);


private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) 
    String token = request.getHeader(HEADER_STRING);
    if (token != null) 
        // parse the token.
        String user;
        try 
            user = Jwts.parser()
                    .setSigningKey(SECRET.getBytes())
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody()
                    .getSubject();
         catch (SignatureException e) 
            return null;
        

        //Get your user
        UserEntity userEntity = this.userRepository.findByUsername(user);

        if (user != null) 
             //Seting in your AuthenticationPrincipal the user
             return new UsernamePasswordAuthenticationToken(userEntity, null, new ArrayList<>());
        

        return null;
    
    return null;



【讨论】:

【参考方案2】:

检查您是否使用了合适的注释,因为其中之一已被弃用。

Documentation - deprecated!

Documentation - fine!


此外,请注意将用户名(字符串)解析为参数,而不是用户类型:

用于将 Authentication.getPrincipal() 解析为方法参数的注解。

Check this topic as well!可以帮上忙。


我不知道这是否是好的做法(我在 Spring 中还没有被认为是“专业人士”),但在我的个人项目中,我从控制器参数中传递的 HttpServletRequest 对象获取令牌。然后我使用 JwtTokenUtil 类,它有getUserFormToken(String token); 方法来解析用户/用户名。它看起来像这样:

控制器

@Autowired
TestService testService;

@Autowired
UserService userService;

@Autowired
private JwtTokenUtil jwtTokenUtil;

@RequestMapping(value="/test", method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public List<Test> getTestsListByUserId(HttpServletRequest req)
    String token = req.getHeader(HEADER_STRING).replace(TOKEN_PREFIX,"");
    return testService.findByUserId(userService.findByUsername(jwtTokenUtil.getUsernameFromToken(token)));

JwtTokenUtil

@Component
public class JwtTokenUtil implements Serializable 

public String getUsernameFromToken(String token) 
    return getClaimFromToken(token, Claims::getSubject);


public Date getExpirationDateFromToken(String token) 
    return getClaimFromToken(token, Claims::getExpiration);


public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) 
    final Claims claims = getAllClaimsFromToken(token);
    return claimsResolver.apply(claims);


private Claims getAllClaimsFromToken(String token) 
    return Jwts.parser()
            .setSigningKey(SIGNING_KEY)
            .parseClaimsJws(token)
            .getBody();


private Boolean isTokenExpired(String token) 
    final Date expiration = getExpirationDateFromToken(token);
    return expiration.before(new Date());


public String generateToken(User user) 
    return doGenerateToken(user.getUsername());


private String doGenerateToken(String subject) 

    Claims claims = Jwts.claims().setSubject(subject);
    claims.put("scopes", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));

    return Jwts.builder()
            .setClaims(claims)
            .setIssuer("issuer")
            .setIssuedAt(new Date(System.currentTimeMillis()))
            .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000))
            .signWith(SignatureAlgorithm.HS256, SIGNING_KEY)
            .compact();


public Boolean validateToken(String token, UserDetails userDetails) 
    final String username = getUsernameFromToken(token);
    return (
           username.equals(userDetails.getUsername())
                   && !isTokenExpired(token));
    


但我通常会根据你的不同过滤器实现。如果您有兴趣 - 我使用了this 教程和实现。

【讨论】:

【参考方案3】:

要检索自定义模型,我会执行以下操作:

从数据库中获取模型并将其设置为主体。

 private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) 
        String token = request.getHeader(HEADER_STRING);
        if (token != null) 
            // parse the token.
            String user;
            try 
                user = Jwts.parser()
                        .setSigningKey(SECRET.getBytes())
                        .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                        .getBody()
                        .getSubject();
             catch (SignatureException e) 
                return null;
            

            // Get user model
            ApplicationUser userModel = userRepository.findByUsername(user);

            // Set it
            if (user != null && userModel != null) return new UsernamePasswordAuthenticationToken(userModel, null, new ArrayList<>());

            return null;
        
        return null;
    

然后在控制器中使用@AuthenticationPrincipal注解检索。

public ApplicationUser getCurrentUser(@AuthenticationPrincipal ApplicationUser user) 
    return user;

【讨论】:

【参考方案4】:

如果这仍然是实际的,我刚刚回答了类似的问题here

重点是从标题中取出authorization token

    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
    String token = request.getHeader("Authorization").split(" ")[1];

之后,您可以对其进行解码并获得您需要的部分。

【讨论】:

以上是关于Spring with JWT auth,获取当前用户的主要内容,如果未能解决你的问题,请参考以下文章

Spring boot + JWT 实现安全验证 ---auth0.jwt

Spring boot JWT Auth 详细信息为空

带有 JWT auth 和 csrf 令牌的 Spring Boot STATELESS 应用程序

有没有办法在同一个项目中同时使用 auth0-spring-security-api 和 java-jwt?

Auth0 Laravel 获取 JWT 令牌

Spring Cloud authentication with JWT service