Spring Security + JWT 实现基于Token的安全验证

Spring Security + JWT 实现基于token的安全验证

准备工做

使用Maven搭建SpringMVC项目,并加入Spring Security的实现 web

JWT简介

参考: http://www.tuicool.com/articles/R7Rj6r3 
官网: https://jwt.io/introduction/ 算法

JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种能够安全传输的 小巧 和 自包含 的JSON对象。因为数据是使用数字签名的,因此是可信任的和安全的。JWT可使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。spring

JWT的结构

JWT包含了使用 . 分隔的三部分: 
1.Header 头部,包含了两部分:token类型和采用的加密算法。 
2.Payload 负载,Token的第二部分是负载,它包含了claim, Claim是一些实体(一般指的用户)的状态和额外的元数据。 
3.Signature 签名,建立签名须要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。 json

下面是一个jjwt生成的token缓存

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJkZmRzYSIsImNyZWF0ZWQiOjE0OTQ5MjgzODQ1MzksInJvbGVzIjpbeyJhdXRob3JpdHkiOiJST0xFX0FOT05ZTU9VUyJ9LHsiYXV0aG9yaXR5IjoiUk9MRV9BRE1JTiJ9LHsiYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn0seyJhdXRob3JpdHkiOiJST0xFX0RCQSJ9XSwiaWQiOjAsImV4cCI6MTQ5NTUzMzE4NH0.RAWhCcFj7sfXI81zJ8fm0Rfb0IpwT7mNfuFPGzU6AblW2UdOgMtDExXlWZEr3pracdytsfw3os4dnJKM6ZW9mA

经过base64解码上面token能够获得基本信息。 
第一段为Header信息,第二段为Payload信息,最后一段实际上是签名,这个签名必须知道秘钥才能计算。这个也是JWT的安全保障。 
注意事项,因为数据声明(Claim)是公开的,千万不要把密码等敏感字段放进去。 安全

{"alg":"HS512"}{"sub":"dfdsa","created":1494928384539,"roles":[{"authority":"ROLE_ANONYMOUS"},{"authority":"ROLE_ADMIN"},{"authority":"ROLE_USER"},{"authority":"ROLE_DBA"}],"id":0,"exp":1495533184}hBpX뱵ȳ\ɱ鴅洢쓮c_蓆͎க摓ಐąyVdJ禶췫l
資g$㺥of

JWT的工做流程


1.用户携带username和password请登陆 
2.服务器验证登陆验证,若是验证成功,根据用户的信息和服务器的规则生成JWT Token 
3.服务器将该token返回 
4.用户获得token,存在localStorage、cookie或其它数据存储形式中。 
5.之后用户请求服务器时,在请求的header中加入 Authorization: Bearer xxxx(token) 。此处注意token以前有一个7字符长度的“Bearer “,服务器端对此token进行检验,若是合法就解析其中内容,根据其拥有的权限和业务逻辑反回响应结果。 服务器

实现JWT支持

添加Jar

<dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.6.0</version>
    </dependency>

建立JwtTokenUtils

private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_ID = "id";
    private static final String CLAIM_KEY_CREATED = "created";
    private static final String CLAIM_KEY_ROLES = "roles";

    @Value("${jwt.token.secret}")
    private String secret;

    @Value("${jwt.token.expiration}")
    private int expiration; //过时时长,单位为秒,能够经过配置写入。

    public String getUsernameFromToken(String token) {
        String username;
        try {
            username =getClaimsFromToken(token).getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    public Date getCreatedDateFromToken(String token) {
        Date created;
        try {
            final Claims claims = getClaimsFromToken(token);
            created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
        } catch (Exception e) {
            created = null;
        }
        return created;
    }

    public Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }

    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

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

    public String generateToken(User userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        claims.put(CLAIM_KEY_ID, userDetails.getId());
        claims.put(CLAIM_KEY_ROLES, userDetails.getAuthorities());
        return generateToken(claims);
    }

    public String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Boolean canTokenBeRefreshed(String token) {
        return !isTokenExpired(token);
    }

    public String refreshToken(String token) {
        String refreshedToken;
        try {
            final Claims claims = getClaimsFromToken(token);
            claims.put(CLAIM_KEY_CREATED, new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        User user = (User) userDetails;
        final String username = getUsernameFromToken(token);
        final Date created = getCreatedDateFromToken(token);
        return (
                username.equals(user.getUsername())
                        && isTokenExpired(token)==false);
    }
 

修改WebSecurityConfig

@Configuration
@EnableWebSecurity
//添加annotation 支持,包括(prePostEnabled,securedEnabled...)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
                // 因为使用的是JWT,咱们这里不须要csrf
                csrf().disable()

                // 基于token,因此不须要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .sessionFixation().none()

                //全部用户能够访问"/resources"目录下的资源以及访问"/home"和favicon.ico
                .antMatchers("/resources/**", "/home","/**/favicon.ico","/auth/*").permitAll()

                //以"/admin"开始的URL,并需拥有 "ROLE_ADMIN" 角色权限,这里用hasRole不须要写"ROLE_"前缀;
                .antMatchers("/admin/**").hasRole("ADMIN")
                //以"/admin"开始的URL,并需拥有 "ROLE_ADMIN" 角色权限和 "ROLE_DBA" 角色,这里不须要写"ROLE_"前缀;
                .antMatchers("/dba/**").access("hasRole('ADMIN') and hasRole('DBA')")

                //前面没有匹配上的请求,所有须要认证;
                .anyRequest().authenticated()

                .and()
                //指定登陆界面,而且设置为全部人都能访问;
                .formLogin().loginPage("/login").permitAll()
                //若是登陆失败会跳转到"/hello"
                .successForwardUrl("/hello")
                .successHandler(loginSuccessHandler())
                //若是登陆失败会跳转到"/logout"
                //.failureForwardUrl("/logout")

                .and()
                .logout()
                .logoutUrl("/admin/logout") //指定登出的地址,默认是"/logout"
                .logoutSuccessUrl("/home")   //登出后的跳转地址login?logout
                 //自定义LogoutSuccessHandler,在登出成功后调用,若是被定义则logoutSuccessUrl()就会被忽略
                .logoutSuccessHandler(logoutSuccessHandler())
                .invalidateHttpSession(true)  //定义登出时是否invalidate HttpSession,默认为true
                //.addLogoutHandler(logoutHandler) //添加自定义的LogoutHandler,默认会添加SecurityContextLogoutHandler
                .deleteCookies("usernameCookie","urlCookie") //在登出同时清除cookies
                ;

        // 禁用缓存
        http.headers().cacheControl();

        // 添加JWT filter
        http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

    }

    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                // 设置UserDetailsService
                .userDetailsService(this.userDetailsService)
                // 使用MD5进行密码的加密
                .passwordEncoder(passwordEncoder());
    }

    private Md5PasswordEncoder passwordEncoder() {
        return new Md5PasswordEncoder();
    }


    private AccessDeniedHandler accessDeniedHandler(){
        AccessDeniedHandlerImpl handler = new AccessDeniedHandlerImpl();
        handler.setErrorPage("/login");
        return handler;
    }

    @Bean
    public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
        return new JwtAuthenticationTokenFilter();
    }


    @Bean
    public LoginSuccessHandler loginSuccessHandler(){
        LoginSuccessHandler handler = new LoginSuccessHandler();
        return  handler;
    }

    @Bean
    public LogoutSuccessHandler logoutSuccessHandler(){
        return  new LogoutSuccessHandler();
    }
}

建立JwtAuthenticationTokenFilter

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private final static Logger logger = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);

    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private JwtTokenUtils jwtTokenUtils;
    @Resource
    private UserRepository userRepository;

    private String tokenHeader = "Authorization";

    private String tokenHead = "Bearer ";

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain) throws ServletException, IOException {

        //先从url中取token
        String authToken = request.getParameter("token");
        String authHeader = request.getHeader(this.tokenHeader);
        if (StringUtils.isNotBlank(authHeader) && authHeader.startsWith(tokenHead)) {
            //若是header中存在token,则覆盖掉url中的token
            authToken = authHeader.substring(tokenHead.length()); // "Bearer "以后的内容
        }

        if (StringUtils.isNotBlank(authToken)) {
            String username = jwtTokenUtils.getUsernameFromToken(authToken);

            logger.info("checking authentication {}", username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                //从已有的user缓存中取了出user信息
                User user = userRepository.findByUsername(username);

                //检查token是否有效
                if (jwtTokenUtils.validateToken(authToken, user)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());

                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                    //设置用户登陆状态
                    logger.info("authenticated user {}, setting security context",username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

建立LoginSuccessHandler

public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    protected Logger logger = LoggerFactory.getLogger(LoginSuccessHandler.class);

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private JwtTokenUtils jwtTokenUtils;
    @Resource
    private UserRepository userRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException {
        final User userDetails = (User)userDetailsService.loadUserByUsername(authentication.getName());
        final String token = jwtTokenUtils.generateToken(userDetails);
        userRepository.insert(userDetails);
        handle(request, response, authentication,token);
        clearAuthenticationAttributes(request);
    }

    protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication,String token)
            throws IOException {
        String targetUrl = determineTargetUrl(authentication);
        if (response.isCommitted()) {
            logger.debug(
                    "Response has already been committed. Unable to redirect to "
                            + targetUrl);
            return;
        }
        redirectStrategy.sendRedirect(request, response, targetUrl+"?token="+token);
    }

    /**
     *
     * 实现自定义的跳转逻辑
     *
     * @param authentication
     * @return
     */
    protected String determineTargetUrl(Authentication authentication) {
        boolean isUser = false;
        boolean isAdmin = false;
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        for (GrantedAuthority grantedAuthority : authorities) {
            if (grantedAuthority.getAuthority().equals("ROLE_USER")) {
                isUser = true;
                break;
            } else if (grantedAuthority.getAuthority().equals("ROLE_ADMIN")) {
                isAdmin = true;
                break;
            }
        }
        if (isUser) {
            return "/websocket";
        } else if (isAdmin) {
            return "/stomp";
        } else {
            throw new IllegalStateException();
        }
    }

    protected void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return;
        }
        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }
}

建立LogoutSuccessHandler

public class LogoutSuccessHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler {

    protected Logger logger = LoggerFactory.getLogger(LogoutSuccessHandler.class);
    @Resource
    private UserRepository userRepository;

    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        logger.info("logout user {}",authentication.getName());
        //登出后清除用户缓存信息
        userRepository.remove(authentication.getName());
    }
}

建立UserRepository

UserRepository只有一个map,缓存用户信息,实际工做中能够引入真实缓存工具来实现。websocket

/**
 * 存入user token,能够引用缓存系统,存入到缓存。
 */
@Component
public class UserRepository {

    private static final Map<String,User> userMap = new HashMap<String,User>();

    public User findByUsername(final String username){
        return userMap.get(username);
    }

    public User insert(User user){
        userMap.put(user.getUsername(),user);
        return user;
    }

    public void remove(String username){
        userMap.remove(username);
    }
}