天天看點

springsecurity-jwt整合

作者:實戰Java

2.1整合springsecurity

1)添加依賴

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
           

2.2認證授權流程

springsecurity-jwt整合

認證管理

springsecurity-jwt整合

流程圖解讀:

1、使用者送出使用者名、密碼被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 過濾器擷取到, 封裝為請求Authentication,通常情況下是UsernamePasswordAuthenticationToken這個實作類。

2、然後過濾器将Authentication送出至認證管理器(AuthenticationManager)進行認證 。

3、認證成功後, AuthenticationManager 身份管理器傳回一個被填充滿了資訊的(包括上面提到的權限資訊, 身份資訊,細節資訊,但密碼通常會被移除) Authentication 執行個體。

4、SecurityContextHolder 安全上下文容器将第3步填充了資訊的 Authentication ,通過 SecurityContextHolder.getContext().setAuthentication(…)方法,設定到其中。可以看出AuthenticationManager接口(認證管理器)是認證相關的核心接口,也是發起認證的出發點,它 的實作類為ProviderManager。而Spring Security支援多種認證方式,是以ProviderManager維護着一個 List 清單,存放多種認證方式,最終實際的認證工作是由 AuthenticationProvider完成的。咱們知道web表單的對應的AuthenticationProvider實作類為 DaoAuthenticationProvider,它的内部又維護着一個UserDetailsService負責UserDetails的擷取。最終 AuthenticationProvider将UserDetails填充至Authentication。

授權管理

springsecurity-jwt整合

通路資源(即授權管理),通路url時,會通過FilterSecurityInterceptor攔截器攔截,其中會調用SecurityMetadataSource的方法來擷取被攔截url所需的全部權限,再調用授權管理器AccessDecisionManager,這個授權管理器會通過spring的全局緩存SecurityContextHolder擷取使用者的權限資訊,還會擷取被攔截的url和被攔截url所需的全部權限,然後根據所配的投票政策(有:一票決定,一票否定,少數服從多數等),如果權限足夠,則決策通過,傳回通路資源,請求放行,否則跳轉到403頁面、自定義頁面。

2.3編寫自己的UserDetails和UserDetailService

2.3.1UserDetails

package com.ds.book.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Collection;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

/**
 * <p>
 * 
 * </p>
 *
 * @author java大師
 * @since 2023-03-17
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_user")
public class User implements Serializable, UserDetails {

    private static final long serialVersionUID = 1L;
    
    private Integer id;

    /**
     * 登入名
     */
    private String name;

    /**
     * 使用者名
     */
    private String username;

    /**
     * 密碼
     */
    private String password;

    /**
     * 是否有效:1-有效;0-無效
     */
    private String status;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles
                .stream()
                .map(role -> new SimpleGrantedAuthority(role.getRoleCode()))
                .collect(Collectors.toList());
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
           

2.3.2userDetailService

登入成功後,将UserDetails的roles設定到使用者中

package com.ds.book.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ds.book.entity.User;
import com.ds.book.mapper.UserMapper;
import com.ds.book.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

/**
 * <p>
 *  服務實作類
 * </p>
 *
 * @author java大師
 * @since 2023-03-17
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService, UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User loginUser = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
        if (loginUser == null){
            throw new UsernameNotFoundException("使用者名或密碼錯誤");
        }
        loginUser.setRoles(userMapper.getRolesByUserId(loginUser.getId()));
        return loginUser;
    }
}
           

2.3.2加載userDetailService

将我們自己的UserDetailService注入springsecurity

package com.ds.book.config;

import com.ds.book.filter.JwtTokenFilter;
import com.ds.book.service.impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
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.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    private UserServiceImpl userService;

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

    //注入我們自己的UserDetailService
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }
}
           

問題:前後端分離項目,通常不會使用springsecurity自帶的登入界面,登入界面由前端完成,背景隻需要提供響應的服務即可,且目前主流不會采用session去存取使用者,後端會傳回響應的token,前端通路的時候,會在headers裡面帶入token.

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-pW7SxQqz-1679881634010)(D:\個人\公衆号\網站\dsblog開發手冊\image-20230322114008186.png)]

2.4JwtToken

2.4.1 JWT描述

Jwt token由Header、Payload、Signature三部分組成,這三部分之間以小數點”.”連接配接,JWT token長這樣:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU

token解析後長這樣:

header部分,有令牌的類型(JWT)和簽名算法名稱(HS256):

{

“alg”: “HS256”,

“typ”: “JWT”

}

Payload部分,有效負載,這部分可以放任何你想放的資料:

{

“sub”: “1234567890”,

“name”: “John Doe”,

“iat”: 1516239022

}

Signature簽名部分,由于這部分是使用header和payload部分計算的,是以還可以以此來驗證payload部分有沒有被篡改:

HMACSHA256(

base64UrlEncode(header) + “.” +

base64UrlEncode(payload),

123456 //這裡是密鑰,隻要夠複雜,一般不會被破解

)

2.4.2 pom.xml

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

2.4.3 JwtToken工具類

package com.ds.book.tool;


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

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
 * JWT工具類
 */
public class JwtUtil {

    //有效期為
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一個小時
    //設定秘鑰明文
    public static final String JWT_KEY = "dashii";

    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }

    /**
     * 生成jtw
     * @param subject token中要存放的資料(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 設定過期時間
        return builder.compact();
    }

    /**
     * 生成jtw
     * @param subject token中要存放的資料(json格式)
     * @param ttlMillis token逾時時間
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 設定過期時間
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis= JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主題  可以是JSON資料
                .setIssuer("dashi")     // 簽發者
                .setIssuedAt(now)      // 簽發時間
                .signWith(signatureAlgorithm, secretKey) //使用HS256對稱加密算法簽名, 第二個參數為秘鑰
                .setExpiration(expDate);
    }

    /**
     * 建立token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 設定過期時間
        return builder.compact();
    }

    /**
     * 生成加密後的秘鑰 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}
           

2.4.4 JwtTokenFilter

package com.ds.book.filter;

import com.ds.book.entity.User;
import com.ds.book.mapper.UserMapper;
import com.ds.book.service.IMenuService;
import com.ds.book.service.IUserService;
import com.ds.book.tool.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

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

@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    @Autowired
    private IUserService userService;
    @Autowired
    private UserMapper userMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //1、擷取token
        String token = httpServletRequest.getHeader("token");
        if (StringUtils.isEmpty(token)){
            filterChain.doFilter(httpServletRequest,httpServletResponse);
            return;
        }
        String userId;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception exception) {
            exception.printStackTrace();
            throw new RuntimeException("token非法");
        }
        User user = userService.getUserById(Integer.parseInt(userId));
        user.setRoles(userMapper.getRolesByUserId(Integer.parseInt(userId)));
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

           

在springsecurity中,第一個經過的過濾器是UsernamePasswordAuthenticationFilter,是以前後端分離的項目,我們自己定義的過濾器要放在這個過濾器前面,具體配置如下

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated();
        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.cors();
    }
           

2.4.5授權

2.4.5.1 開啟preAuthorize進行收取(Controller路徑比對)

1)主啟動類上添加EnableGlobalMethodSecurity注解

@EnableGlobalMethodSecurity(prePostEnabled = true)
@SpringBootApplication
@MapperScan("com.ds.book.mapper")
public class BookSysApplication {
    public static void main(String[] args) {
        SpringApplication.run(BookSysApplication.class,args);
    }
}
           

2)Controller方法上添加@PreAuthorize注解

@RestController
public class HelloController {

    @GetMapping("/hello")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public String hello(){
        return "hello";
    }
}
           

2.4.5.2 增強方式授權(資料庫表配置)

1)建立我們自己的FilterInvocationSecurityMetadataSource,實作getAttributes方法,擷取請求url所需要的角色

@Component
public class MySecurtiMetaDataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private IMenuService menuService;
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    //擷取通路url需要的角色,例如:/sys/user需要ROLE_ADMIN角色,通路sys/user時擷取到必須要有ROLE_ADMIN角色。傳回		Collection<ConfigAttribute>
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
        //擷取所有的菜單及角色
        List<Menu> menus = menuService.getMenus();
        for (Menu menu : menus) {
            if (antPathMatcher.match(menu.getUrl(),requestURI)){
                String[] roles = menu.getRoles().stream().map(role -> role.getRoleCode()).toArray(String[]::new);
                return SecurityConfig.createList(roles);
            }
        }
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return false;
    }
}
           

2)建立我們自己的決策管理器AccessDecisionManager,實作decide方法,判斷步驟1)中擷取到的角色和我們目前登入的角色是否相同,相同則允許通路,不相同則不允許通路,

@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    
    //1、認證通過後,會往authentication中填充使用者資訊
    //2、拿authentication中的權限與上一步擷取到的角色資訊進行比對,比對成功後,允許通路
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        for (ConfigAttribute configAttribute : configAttributes) {
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(configAttribute.getAttribute())){
                    return;
                }
            }
        }
        throw new AccessDeniedException("權限不足,請聯系管理者");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return false;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return false;
    }
}
           

3)在SecurityConfig中,添加後置處理器(增強器),讓springsecurity使用我們自己的datametasource和decisionMananger

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MySecurtiMetaDataSource mySecurtiMetaDataSource;
    @Autowired
    private MyAccessDecisionManager myAccessDecisionManager;
    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Autowired
    private UserServiceImpl userService;

    @Autowired
    private JwtTokenFilter jwtTokenFilter;

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
            	//後置處理器,使用我們自己的FilterSecurityInterceptor攔截器配置
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor> () {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setSecurityMetadataSource(mySecurtiMetaDataSource);
                        o.setAccessDecisionManager(myAccessDecisionManager);
                        return o;
                    }
                })
                .and()
                .headers().cacheControl();
        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.cors();
    }
}
           

2.4.6異常處理

1)前端渲染工具類

public class WebUtils
{
    /**
     * 将字元串渲染到用戶端
     *
     * @param response 渲染對象
     * @param string 待渲染的字元串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }
}
           

2)未登入異常處理,實作commence方法

@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        Result result = new Result(401,"未登入,請先登入",null);
        String json = JSON.toJSONString(result);
        WebUtils.renderString(httpServletResponse,json);

    }
}
           
springsecurity-jwt整合

3)授權失敗異常處理,實作Handle方法

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        Result result = new Result(403,"權限不足請聯系管理者",null);
        String s = JSON.toJSONString(result);
        WebUtils.renderString(httpServletResponse,s);
    }
}
           
springsecurity-jwt整合

原文連結;https://mp.weixin.qq.com/s/39z49TnZ21Wkyz8jhf_XGQ

繼續閱讀