SpringHomeSpring SecurityJava后台

Spring Boot+Spring security+Jwt实

2018-11-08  本文已影响189人  double_Ji

主要实现的功能是Login拿到token,再用token请求资源。关于登录用户名密码验证这个在另一篇文章有提到(Spring Boot + Security实现简单验证登录操作),这里就主要讲token的生成,验证以及用户具体权限的验证。

GitHub代码地址

本例子功能如下图:


1.引入Spring Security Jwt依赖。

        <!--spring security -->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

2.创建JwtTokenProvider类提供生成以及验证token的方法。

生成token主要用到四个元素:

1)username token主要的标志

2)expireTime token过期时间(xxx ms)

3)issuedDate token的创建时间

4)signWith token签名,包括签名方法和密钥

这里的过期时间和密钥配在properties文件里面,代码通过@Value拿的。

jwtTokenSecret = Sayo
tokenExpiredMs = 604800000
@Component
@PropertySource("classpath:auth.properties")
public class AuthParameters {

    private String jwtTokenSecret;
    private long tokenExpiredMs;

    public String getJwtTokenSecret() {
        return jwtTokenSecret;
    }

    @Value("${jwtTokenSecret}")
    public void setJwtTokenSecret(String jwtTokenSecret) {
        this.jwtTokenSecret = jwtTokenSecret;
    }

    public long getTokenExpiredMs() {
        return tokenExpiredMs;
    }

    @Value("${tokenExpiredMs}")
    public void setTokenExpiredMs(long tokenExpiredMs) {
        this.tokenExpiredMs = tokenExpiredMs;
    }
}
验证token用同样的密钥去解开token

倘若能解开则表示该token是合法可用的,解析时有可能会抛出以下5个exception,可以分别catch处理log出日志,这里都统一处理了。

1)ExpiredJwtException token时效过期异常

2)UnsupportedJwtException 验证的token和期待的token格式不一样时,例如解析的是一个明文JWT而期待的是一个加密签名JWT时就会抛出这个异常。

3)MalformedJwtException 表示这不是一个正确方法创建的token。

4)SignatureException token签名验证失败异常

5)IllegalArgumentException token为null或者空异常


@Component

public class JwtTokenProvider {

    Loggerlogger = LoggerFactory.getLogger(JwtTokenProvider.class);

    @Autowired

    private AuthParametersauthParameters;

    /**

     * Generate token for user login.

     *

     * @param authentication

     * @return return a token string.

     */

     public String createJwtToken(Authentication authentication) {

        //user name

        String username = ((org.springframework.security.core.userdetails.User) authentication.getPrincipal()).getUsername();

        //expire time

        Date expireTime =new Date(System.currentTimeMillis()+authParameters.getTokenExpiredMs());

        //create token

        String token = Jwts.builder()

                      .setSubject(username)

                      .setExpiration(expireTime)

                      .setIssuedAt(new Date())

                      .signWith(SignatureAlgorithm.HS512, authParameters.getJwtTokenSecret())

                      .compact();

         return token;

    }

    /**

     * validate token eligible.

     * if Jwts can parse the token string and no throw any exception, then the token is eligible.

     * @param token a jws string.

*/

    public boolean validateToken(String token) {

        String VALIDATE_FAILED ="validate failed : ";

        try {
              Jwts.parser()
                  .setSigningKey(authParameters.getJwtTokenSecret())
                  .parseClaimsJws(token);

              return true;

        }catch (Exception ex) {

              //ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, //IllegalArgumentException

              logger.error(VALIDATE_FAILED + ex.getMessage());

              return false;

        }

}

}

image

3.创建JwtAuthenticationFilter类在用户获取资源之前让spring去filter这个token是否合法可用。

继承OncePerRequestFilter重写doFilterInternal方法,前端发送请求时,token会放在header,在每个请求读取资源之前后台对token进行filter。

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Autowired
    private AuthParameters authParameters;

    @Autowired
    private UserService userService;

    //1.从每个请求header获取token
    //2.调用前面写的validateToken方法对token进行合法性验证
    //3.解析得到username,并从database取出用户相关信息权限
    //4.把用户信息(role等)以UserDetail形式放进SecurityContext以备整个请求过程使用。
    // (例如哪里需要判断用户权限是否足够时可以直接从SecurityContext取出去check)
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = getJwtFromRequest(request);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            String username = getUsernameFromJwt(token, authParameters.getJwtTokenSecret());

            UserDetails userDetails = userService.getUserDetailByUserName(username);

            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    userDetails,null,userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);

        } else {
            logger.error(request.getParameter("username") + " :Token is null");
        }
        super.doFilter(request, response, filterChain);
    }

    /**
     * Get Bear jwt from request header Authorization.
     *
     * @param request servlet request.
     * @return token or null.
     */
    private String getJwtFromRequest(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer")) {
            return token.replace("Bearer ", "");
        }
        return null;
    }

    /**
     * Get user name from Jwt, the user name have set to jwt when generate token.
     *
     * @param token jwt token.
     * @param signKey jwt sign key, set in properties file.
     * @return user name.
     */
    private String getUsernameFromJwt(String token, String signKey) {
        return Jwts.parser().setSigningKey(signKey)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
}

上面调用到的getUserDetailByUserName在UserService

    /**
     * Get {@link UserDetails} by user name.
     * @return
     */
    @Transactional
    public UserDetails getUserDetailByUserName(String username){

        User user = this.userRepository.findByUserName(username);

        if(user == null){
            //throw exception inform front end not this user
            throw new UsernameNotFoundException("user + " + username + "not found.");
        }

        List<String> roleList = this.userRepository.queryUserOwnedRoleCodes(username);
        List<GrantedAuthority> authorities = roleList.stream()
                .map(role -> new SimpleGrantedAuthority(role)).collect(Collectors.toList());

        return new org.springframework.security.core.userdetails
                .User(username,user.getPassword(),authorities);
    }

4.配置HttpSecurity

其他配置说明在文章开头提到的另一篇文章中有写,这里只说新添加的配置
1)添加注解@EnableGlobalMethodSecurity,并设置prePostEnabled为true(默认是false),启用Spring security的前注解(例如本例用到的@PreAuthorize)
2)把自定义的JwtAuthenticationFilter添加到UsernamePasswordAuthenticationFilter之前。
3)因为我们使用了token,所以session要禁止掉创建和使用,不然会白白耗掉很多空间,SessionCreationPolicy设为STATELESS,即永不创建HttpSession并且不会使用HttpSession去获取SecurityContext。


WebSecurityConfig.java
WebSecurityConfig.java

5.登陆成功在AuthenticationSuccessHandler返回token给前端

@Service("authenticationSuccessHandler")
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private JwtTokenProvider tokenProvider;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response
            , Authentication authentication) throws IOException {
        logger.info("User: " + authentication.getName() + " Login successfully.");
        this.returnJson(response,authentication);
    }

    private void returnJson(HttpServletResponse response,Authentication authentication) throws IOException {
        response.setStatus(HttpServletResponse.SC_OK);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter()
                .println("{\"tokenType\":\"Bearer\",\"token\": \"" + tokenProvider.createJwtToken(authentication) + "\"}");
    }
}

6.在Controller方法加上具体权限限制

用@PreAuthorize("hasAuthority('role')"),进行方法级别验证登录user的是否有足够的权限访问该方法,这里举例用的是admin权限。

@RestController
@RequestMapping("/api")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping(value = "/user")
    @PreAuthorize("hasAuthority('admin')")
    public UserView getUserByName(@RequestParam("userName") String userName) {
        return userService.getUserByUserName(userName);
    }
}

hasAuthority在spring中的源码,主要是在authentication中拿到当前user所拥有的role然后再check是否包含有访问这个方法需要的role。

    public final boolean hasAuthority(String authority) {
        return hasAnyAuthority(authority);
    }

    public final boolean hasAnyAuthority(String... authorities) {
        return hasAnyAuthorityName(null, authorities);
    }
    private boolean hasAnyAuthorityName(String prefix, String... roles) {
        Set<String> roleSet = getAuthoritySet();

        for (String role : roles) {
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }

        return false;
    }

7. 测试

先看看我数据库的数据是这样的。


user table
role table
user_map_role table
登陆带admin权限的用户,成功获取资源

1)登录


login-Sayo

2)请求资源在Header带上key为Authorization,value为Bearer +token,因为当前登录的用户Sayo在数据库是带有admin权限所以成功获得数据。


request
登录不带admin权限的用户,无法获取资源,返回权限不够提示

1)登录


login-Ly

2)当前用户不带admin权限,而该方法配置了需要admin权限访问,请求资源失败


request
上一篇下一篇

猜你喜欢

热点阅读