Spring Security + JWT 實現基於Token的安全驗證

Spring Security + JWT 實現基於token的安全驗證

準備工作

使用Maven搭建SpringMVC項目,並加入Spring Security的實現 

JWT簡介

參考: http://www.tuicool.com/articles/R7Rj6r3 
官網: https://jwt.io/introduction/ 

JWT是 Json Web Token 的縮寫。它是基於 RFC 7519 標準定義的一種可以安全傳輸的 小巧 和 自包含 的JSON對象。由於數據是使用數字簽名的,所以是可信任的和安全的。JWT可以使用HMAC算法對secret進行加密或者使用RSA的公鑰私鑰對來進行簽名。

JWT的結構

JWT包含了使用 . 分隔的三部分: 
1.Header 頭部,包含了兩部分:token類型和採用的加密算法。 
2.Payload 負載,Token的第二部分是負載,它包含了claim, Claim是一些實體(通常指的用戶)的狀態和額外的元數據。 
3.Signature 簽名,創建簽名需要使用編碼後的header和payload以及一個祕鑰,使用header中指定簽名算法進行簽名。 

下面是一個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).and()
                .authorizeRequests()

                //所有用戶可以訪問"/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,緩存用戶信息,實際工作中可以引入真實緩存工具來實現。

/**
 * 存入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);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章