第七章:使用jwt token的方式來進行登錄

我們之前都在使用http session的方式來進行登錄訪問的。那麼現在流行的是前後端分離的開發模式。而且我們也打算這樣幹。包括後期我們的小程序啊什麼的,很多都不支持session的方式。那我們就使用token的模式來進行登錄。

jwttoken

爲什麼要使用這個jwttoken呢?

  • 安全性高,防止token被僞造和篡改
  • 自包含,減少存儲開銷
  • 跨語言,支持多種語言的實現
  • 支持過期,發佈者校驗

主要有上面幾點優勢。安全性跟減少存儲開銷。好了,讓我們來開始使用它吧!

pom

在pom中添加jar的支持

<!-- jwt token -->
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.1</version>
</dependency>

JWT工具類(JwtTokenUtils)

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.List;

/**
 * @author: 胡漢三
 * @date: 2020/5/26 10:11
 * @description: JWT工具類
 * JWT是由三段組成的,分別是header(頭)、payload(負載)和signature(簽名)
 * 其中header中放{
 *   "alg": "HS512",
 *   "typ": "JWT"
 * } 表明使用的加密算法,和token的類型==>默認是JWT
 *
 */
public class JwtTokenUtils {

    public static final String TOKEN_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";

    // 密鑰,用於signature(簽名)部分解密
    private static final String PRIMARY_KEY = "HHS_fast_java_plat";
    // 簽發者
    private static final String ISS = "HanSan.HU";
    // 添加角色的key
    private static final String ROLE_CLAIMS = "role";

    // 過期時間是3600秒,既是1個小時
    private static final long EXPIRATION = 3600L;

    // 選擇了記住我之後的過期時間爲7天
    private static final long EXPIRATION_REMEMBER = 604800L;

    /**
     * description: 創建Token
     *
     * @param username
     * @param isRememberMe
     * @return java.lang.String
     */
    public static String createToken(String username, List<String> roles, boolean isRememberMe) {
        long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
        HashMap<String, Object> map = new HashMap<>();
        map.put(ROLE_CLAIMS, roles);
        return Jwts.builder()
                // 採用HS512算法對JWT進行的簽名,PRIMARY_KEY是我們的密鑰
                .signWith(SignatureAlgorithm.HS512, PRIMARY_KEY)
                // 設置角色名
                .setClaims(map)
                // 設置發證人
                .setIssuer(ISS)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .compact();
    }

    /**
     * description: 從token中獲取用戶名
     *
     * @param token
     * @return java.lang.String
     */
    public static String getUsername(String token){
        return getTokenBody(token).getSubject();
    }

    // 獲取用戶角色
    public static List<String> getUserRole(String token){
        return (List<String>) getTokenBody(token).get(ROLE_CLAIMS);
    }

    /**
     * description: 判斷Token是否過期
     *
     * @param token
     * @return boolean
     */
    public static boolean isExpiration(String token){
        return getTokenBody(token).getExpiration().before(new Date());
    }

    /**
     * description: 獲取
     *
     * @param token
     * @return io.jsonwebtoken.Claims
     */
    private static Claims getTokenBody(String token){
        return Jwts.parser()
                .setSigningKey(PRIMARY_KEY)
                .parseClaimsJws(token)
                .getBody();
    }
}

重寫賬號認證過濾器(UsernamePasswordAuthenticationFilter)

import com.alibaba.fastjson.JSON;
import com.hzw.code.common.utils.ActionResult;
import com.hzw.code.common.utils.JwtTokenUtils;
import com.hzw.code.common.utils.ResultCodeEnum;
import com.hzw.code.security.model.CustomUserDetails;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @author: 胡漢三
 * @date: 2020/5/26 10:16
 * @description: 進行用戶賬號的驗證==>認證功能
 *
 */
public class JwtLoginAuthFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    private ThreadLocal<Boolean> rememberMe = new ThreadLocal<>();

    public JwtLoginAuthFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        // 設置該過濾器地址
        super.setFilterProcessesUrl("/jwt/login");
    }

    /**
     * description: 登錄驗證
     *
     * @param request
     * @param response
     * @return org.springframework.security.core.Authentication
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        rememberMe.set(Boolean.parseBoolean(request.getParameter("rememberMe")));
        return authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getParameter("username"),
                        request.getParameter("password"), new ArrayList<>())
        );
    }

    /**
     * description: 登錄驗證成功後調用,驗證成功後將生成Token,並重定向到用戶主頁home
     * 與AuthenticationSuccessHandler作用相同
     *
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @return void
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {

        // 查看源代碼會發現調用getPrincipal()方法會返回一個實現了`UserDetails`接口的對象,這裏是CustomUserDetails
        CustomUserDetails user = (CustomUserDetails) authResult.getPrincipal();
        System.out.println("CustomUserDetails:" + user.toString());
        boolean isRemember = rememberMe.get();
        List<String> roles = new ArrayList<>();
        Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
        for (GrantedAuthority authority : authorities){
            roles.add(authority.getAuthority());
        }
        System.out.println("roles:"+roles);
        String token = JwtTokenUtils.createToken(user.getUsername(), roles,isRemember);
        System.out.println("token:"+token);

        // 登錄成功
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        ActionResult<String> result = new ActionResult<>(ResultCodeEnum.SUCCESS);
        result.setMessage("登錄成功");
        result.setData(token);
        response.getWriter().write(JSON.toJSONString(result));
    }

    /**
     * description: 登錄驗證失敗後調用,這裏直接Json返回,實際上可以重定向到錯誤界面等
     * 與AuthenticationFailureHandler作用相同
     *
     * @param request
     * @param response
     * @param failed
     * @return void
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        ActionResult<String> result = new ActionResult<>(ResultCodeEnum.LOGIN_FAIL);
        response.getWriter().write(JSON.toJSONString(result));
    }
}

對所有請求進行過濾(BasicAuthenticationFilter)

import com.hzw.code.common.utils.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

/**
 * @author: 胡漢三
 * @date: 2020/5/26 10:20
 * @description: 對所有請求進行過濾
 * BasicAuthenticationFilter繼承於OncePerRequestFilter==》確保在一次請求只通過一次filter,而不需要重複執行。
 */
public class JwtPreAuthFilter extends BasicAuthenticationFilter {

    public JwtPreAuthFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    /**
     * description: 從request的header部分讀取Token
     *
     * @param request
     * @param response
     * @param chain
     * @return void
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {
        System.out.println("BasicAuthenticationFilters");
        String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
        System.out.println("tokenHeader:"+tokenHeader);
        // 如果請求頭中沒有Authorization信息則直接放行了
        if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果請求頭中有token,則進行解析,並且設置認證信息
        SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
        super.doFilterInternal(request, response, chain);
    }

    /**
     * description: 讀取Token信息,創建UsernamePasswordAuthenticationToken對象
     *
     * @param tokenHeader
     * @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        //解析Token時將“Bearer ”前綴去掉
        String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        String username = JwtTokenUtils.getUsername(token);
        List<String> roles = JwtTokenUtils.getUserRole(token);
        Collection<GrantedAuthority> authorities = new HashSet<>();
        if (roles!=null) {
            for (String role : roles) {
                authorities.add(new SimpleGrantedAuthority(role));
            }
        }
        if (username != null){
            return new UsernamePasswordAuthenticationToken(username, null, authorities);
        }
        return null;
    }
}

用戶登出成功時返回

import com.alibaba.fastjson.JSON;
import com.hzw.code.common.utils.ActionResult;
import com.hzw.code.common.utils.ResultCodeEnum;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author: 胡漢三
 * @date: 2020/5/26 10:44
 * @description: 用戶登出成功時返回給前端的數據
 */
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        ActionResult<String> result = new ActionResult<>(ResultCodeEnum.SUCCESS);
        result.setMessage("登出成功");
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }

}

權限不足自定義返回

import com.alibaba.fastjson.JSON;
import com.hzw.code.common.utils.ActionResult;
import com.hzw.code.common.utils.ResultCodeEnum;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author: 胡漢三
 * @date: 2020/5/26 11:16
 * @description: 權限不足自定義返回
 *
 */
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        ActionResult<String> result = new ActionResult<>(ResultCodeEnum.AUTH_LOWER);
        httpServletResponse.getWriter().write(JSON.toJSONString(result));
    }
}

用戶未登錄時返回給前端的數據

import com.alibaba.fastjson.JSON;
import com.hzw.code.common.utils.ActionResult;
import com.hzw.code.common.utils.ResultCodeEnum;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author: 胡漢三
 * @date: 2020/5/26 11:18
 * @description: 用戶未登錄時返回給前端的數據
 */
public class UnAuthorizedEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        ActionResult<String> result = new ActionResult<>(ResultCodeEnum.UNLOGIN);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().write(JSON.toJSONString(result));
    }
}

修改總配置

import com.hzw.code.security.filter.JwtLoginAuthFilter;
import com.hzw.code.security.filter.JwtPreAuthFilter;
import com.hzw.code.security.handler.CustomAccessDeniedHandler;
import com.hzw.code.security.handler.CustomLogoutSuccessHandler;
import com.hzw.code.security.handler.UnAuthorizedEntryPoint;
import com.hzw.code.security.service.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

/**
 * spring security 配置
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true,jsr250Enabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final CustomUserDetailsService userDetailsService;
    public SecurityConfiguration(CustomUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 設置自定義的userDetailsService
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * description: http細節
     *
     * @param http
     * @return void
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 開啓跨域資源共享
        http.cors()
                .and()
                // 關閉csrf
                .csrf().disable()
                // 關閉session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .httpBasic().authenticationEntryPoint(new UnAuthorizedEntryPoint())
                .and()
                .formLogin()
                .loginPage("/login")
                //.successHandler(new Fx)
                .and()
                .logout()//默認註銷行爲爲logout
                .logoutSuccessHandler(new CustomLogoutSuccessHandler())
                .and()
                // 添加到過濾鏈中
                // 先是UsernamePasswordAuthenticationFilter用於login校驗
                .addFilter(new JwtLoginAuthFilter(authenticationManager()))
                // 再通過OncePerRequestFilter,對其他請求過濾
                .addFilter(new JwtPreAuthFilter(authenticationManager()))
                // 自定義權限不足返回
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler());

    }

    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return new CustomAccessDeniedHandler();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }

}

測試

下面我們來測試一下,看看能不能正常登錄

在測試權限:

我們之前在數據庫中設置的權限就是ROLE_ADMIN,那麼說明我們的修改成功了。

 

----------------------------------------------------------

項目的源碼地址:https://gitee.com/gzsjd/fast

----------------------------------------------------------

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章