1. 程式人生 > >Spring Boot整合JWT&Spring Security進行介面安全認證

Spring Boot整合JWT&Spring Security進行介面安全認證

一,協議

token驗證未通過

返回:

{
    "header": {
        "errorinfo": "無效的token",
        "errorcode": "8001"
    }
}

頁面上對這種情況的處理,都跳轉到登陸頁面;

登陸驗證未通過

返回:

{
    "header": {
        "errorinfo": "使用者名稱或密碼錯誤,請重新輸入!",
        "errorcode": "8002"
    }
}

前端頁面對這種情況的處理,清空使用者名稱和密碼,重新輸入;

其他正常情況

按照資料介面的定義,正常互動,參考系統介面協議定義;

JWT

參考文件JWT文件

後端配置

application.yml

# JWT 認證配置
jwt:
  header: Authorization
  secret: w-oasis123456
  expiration: 604800 #token七天不過期
  tokenHead: "Bearer "
  exceptUrl: "/auth/**"

使用者認證相關:

自定義JwtUser,實現spring security 的UserDetails類,用於使用者的認證:

/**
 * \* Created: liuhuichao
 * \* Date: 2017/10/30
 * \* Time: 上午10:32
 * \* Description: 為了安全服務的User
 * \
 */
public class JwtUser implements UserDetails { private final Long id; private final String username; //設定為account private final String password; private final Collection<? extends GrantedAuthority> authorities; public JwtUser(Long id, String username, String password, Collection<? extends GrantedAuthority> authorities) { this
.id = id; this.username = username; this.password = password; this.authorities = authorities; } /** * 返回分配給使用者的角色列表 * @return */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } /** * 賬戶是否未過期 * @return */ @Override public boolean isAccountNonExpired() { return true; } /** * 賬戶是否未鎖定 * @return */ @Override public boolean isAccountNonLocked() { return true; } /** * 密碼是否未過期 * @return */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 賬戶是否啟用 * @return */ @Override public boolean isEnabled() { return true; } }

自定義類,實現UserDetailsService 的認證方法:

/**
 * \* Created: liuhuichao
 * \* Date: 2017/10/30
 * \* Time: 上午10:54
 * \* Description: 提供一種從使用者名稱可以查到使用者並返回的方
 * \
 */
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {

    /**
     * 提供一種從使用者名稱可以查到使用者並返回的方法【本系統使用手機號account進行唯一使用者驗證】
     * @param account
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {

        /**TODO:此處需要寫明從使用者表裡面跟根據使用者account查詢使用者的方法**/
        User user =new User();
        user.setAccount("17319237587");
        user.setPwd("123");
        user.setUserId(1L);
        List<String> roles=new ArrayList<>();
        roles.add("ADMIN");
        user.setRoles(roles);
        return JwtUserFactory.create(user);
    }
}

配置資料庫實體類跟認證類:

/**
 * \* Created: liuhuichao
 * \* Date: 2017/10/30
 * \* Time: 上午10:43
 * \* Description: factory:根據User建立JwtUser
 * \
 */
public final class JwtUserFactory {

    private JwtUserFactory() {
    }

    public static JwtUser create(User user) {
        return new JwtUser(
                user.getUserId(),
                user.getAccount(),//account是唯一的
                user.getPwd(),
                mapToGrantedAuthorities(user.getRoles())
        );
    }

    private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
        return authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

}

token

token操作類:

/**
 * \* Created: liuhuichao
 * \* Date: 2017/10/27
 * \* Time: 下午3:12
 * \* Description:
 * \
 */
@Component
public class JwtUtil {


    private static final String CLAIM_KEY_USER_ACCOUNT = "sub";
    private static final String CLAIM_KEY_CREATED = "created";

    @Value("${jwt.secret}")
    private String secret; //祕鑰

    @Value("${jwt.expiration}")
    private Long expiration; //過期時間

    /**
     * 從token中獲取使用者account
     * @param token
     * @return
     */
    public String getUserAccountFromToken(String token) {
        String useraccount;
        try {
            final Claims claims = getClaimsFromToken(token);
            useraccount = claims.getSubject();
        } catch (Exception e) {
            useraccount = null;
        }
        return useraccount;
    }

    /**
     * 從token中獲取建立時間
     * @param token
     * @return
     */
    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;
    }

    /**
     * 獲取token的過期時間
     * @param token
     * @return
     */
    public Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }

    /**
     * 從token中獲取claims
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 生存token的過期時間
     * @return
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 判斷token是否過期
     * @param token
     * @return
     */
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        Boolean result= expiration.before(new Date());
        return result;
    }



    /**
     * 生成token
     * @param userDetails
     * @return
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USER_ACCOUNT, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

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

    /**
     * token 是否可重新整理
     * @param token
     * @return
     */
    public Boolean canTokenBeRefreshed(String token) {
        final Date created = getCreatedDateFromToken(token);
        return !isTokenExpired(token);
    }

    /**
     * 重新整理token
     * @param token
     * @return
     */
    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;
    }

    /**
     * 驗證token
     * @param token
     * @param userDetails
     * @return
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        JwtUser user = (JwtUser) userDetails;
        final String useraccount = getUserAccountFromToken(token);
        final Date created = getCreatedDateFromToken(token);
        Boolean result= (
                useraccount.equals(user.getUsername())
                        && !isTokenExpired(token)
        );
        return result;
    }
}

驗證token的filter配置:

/**
 * \* Created: liuhuichao
 * \* Date: 2017/10/30
 * \* Time: 上午11:23
 * \* Description: 驗證token
 * \
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @Value("${jwt.header}")
    private String tokenHeader;

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


    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String authHeader = httpServletRequest.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(tokenHead)) {
            final String authToken = authHeader.substring(tokenHead.length()); // The part after "Bearer "
            String useraccount = jwtUtil.getUserAccountFromToken(authToken);
            logger.info("JwtAuthenticationTokenFilter[doFilterInternal] checking authentication " + useraccount);

            if (useraccount != null && SecurityContextHolder.getContext().getAuthentication() == null) {//token校驗通過

                UserDetails userDetails = this.userDetailsService.loadUserByUsername(useraccount);//根據account去資料庫中查詢user資料,足夠信任token的情況下,可以省略這一步

                if (jwtUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
                            httpServletRequest));
                    logger.info("JwtAuthenticationTokenFilter[doFilterInternal]  authenticated user " + useraccount + ", setting security context");
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);

    }
}

配置filter以及攔截url:

/**
 * \* Created: liuhuichao
 * \* Date: 2017/10/30
 * \* Time: 上午11:01
 * \* Description:spring security 的安全配置類
 * \
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Autowired
    private UserDetailsService userDetailsService;

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


    /**
     * 使用者名稱密碼認證方法
     * @param authenticationManagerBuilder
     * @throws Exception
     */
    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                // 設定UserDetailsService
                .userDetailsService(this.userDetailsService);
    }

    /**
     * 裝載BCrypt密碼編碼器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

    /**
     * token請求授權
     * @param httpSecurity
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // 由於使用的是JWT,我們這裡不需要csrf
                .csrf().disable()

                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()//未授權處理

                // 基於token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

                .authorizeRequests()


                // 對於獲取token的rest api要允許匿名訪問
                .antMatchers(exceptUrl).permitAll()

                // 除上面外的所有請求全部需要鑑權認證
                .anyRequest().authenticated();

        // 新增JWT filter
        httpSecurity
                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);  //將token驗證新增在密碼驗證前面

        // 禁用快取
        httpSecurity.headers().cacheControl();
    }

}

處理異常:

/**
 * jwt 未授權
 */
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        JSONObject result=new JSONObject();
        JSONObject header=new JSONObject();
        if(authException instanceof BadCredentialsException){ /**身份認證未通過*/
            header.put("errorcode","8002");
            header.put("errorinfo","使用者名稱或密碼錯誤,請重新輸入!");
            result.put("header",header);
        }else{
            header.put("errorcode","8001");
            header.put("errorinfo","無效的token");
            result.put("header",header);
        }
        response.getWriter().write(JSONObject.toJSONString(result));
    }
}