Spring Security + jwt 許可權系統設計,包含SQL

阿壯Jonsson發表於2020-11-19

完整專案程式碼

https://download.csdn.net/download/y1534414425/13123475

引言

這是一個Spring Security + jwt 做的一個許可權系統設計的demo,註冊預設是使用者角色,沒有登入的情況下不可以訪問使用者和管理員介面,每個角色擁有訪問指定路徑下的介面,管理員許可權繼承自使用者,所以管理員擁有使用者的所有許可權,使用者訪問不了管理員介面,。

一、資料設計

在這裡插入圖片描述

二、關鍵部分程式碼

Spring Security主要配置SecurityConfig

package com.springsecurity.security.config;

import com.springsecurity.security.exception.JwtAccessDeniedHandler;
import com.springsecurity.security.exception.JwtAuthenticationEntryPoint;
import com.springsecurity.security.filter.JwtAuthenticationFilter;
import com.springsecurity.security.filter.JwtAuthorizationFilter;
import com.springsecurity.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
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;

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsServiceImpl;

    /**
     * 密碼編碼器
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 設定自定義的userDetailsService以及密碼編碼器
        auth.userDetailsService(userDetailsServiceImpl)
                // 配置密碼加密規則
                .passwordEncoder(bCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and()
                // 禁用 CSRF
                .csrf().disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/auth/login").permitAll()
                // 指定路徑下的資源需要管理員許可權才能訪問
                .antMatchers("/user/admin/**").hasRole("ADMIN")
                // 指定路徑下的資源需要使用者許可權才能訪問
                .antMatchers("/user/*").hasRole("USER")
                // 其他都放行了
                .anyRequest().permitAll()
                .and()
                //新增自定義Filter
                .addFilter(new JwtAuthenticationFilter(authenticationManager())) // 認證
                .addFilter(new JwtAuthorizationFilter(authenticationManager(), userDetailsServiceImpl)) // 授權
                // 不需要session(不建立會話)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 授權異常處理
                .exceptionHandling().authenticationEntryPoint(new JwtAuthenticationEntryPoint())
                .accessDeniedHandler(new JwtAccessDeniedHandler())
                .and()
                .formLogin().loginProcessingUrl("/login").permitAll();
        // 防止 web 頁面的Frame 被攔截
        http.headers().frameOptions().disable();
    }

    /**
     * 角色繼承
     *
     * @return
     */
    @Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        String hierarchy = "ROLE_ADMIN > ROLE_USER";
        roleHierarchy.setHierarchy(hierarchy);
        return roleHierarchy;
    }
}

認證過濾器

package com.springsecurity.security.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.springsecurity.exception.LoginFailedException;
import com.springsecurity.security.constants.SecurityConstants;
import com.springsecurity.security.dto.LoginRequest;
import com.springsecurity.security.entity.JwtUser;
import com.springsecurity.security.utils.JwtTokenUtils;
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.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 如果使用者名稱和密碼正確,那麼過濾器將建立一個JWT Token 並在HTTP Response 的header中返回它,格式:token: "Bearer +具體token值"
 */
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final ThreadLocal<Boolean> rememberMe = new ThreadLocal<>();
    private final AuthenticationManager authenticationManager;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        // 設定URL,以確定是否需要身份驗證
        super.setFilterProcessesUrl(SecurityConstants.AUTH_LOGIN_URL);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {

        ObjectMapper objectMapper = new ObjectMapper();
        try {
            // 獲取登入的資訊
            LoginRequest loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequest.class);
            rememberMe.set(loginRequest.getRememberMe());
            // 這部分和attemptAuthentication方法中的原始碼是一樣的,
            // 只不過由於這個方法原始碼的是把使用者名稱和密碼這些引數的名字是死的,所以我們重寫了一下
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    loginRequest.getUsername(), loginRequest.getPassword());
            return authenticationManager.authenticate(authentication);
        } catch (IOException | AuthenticationException e) {
            if (e instanceof AuthenticationException) {
                throw new LoginFailedException("登入失敗!請檢查使用者名稱和密碼。");
            }
            throw new LoginFailedException(e.getMessage());
        }
    }

    /**
     * 如果驗證成功,就生成token並返回
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authentication) {

        JwtUser jwtUser = (JwtUser) authentication.getPrincipal();
        List<String> authorities = jwtUser.getAuthorities()
                .stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());
        // 建立 Token
        String token = JwtTokenUtils.createToken(jwtUser.getUsername(), authorities, rememberMe.get());
        rememberMe.remove();
        // Http Response Header 中返回 Token
        response.setHeader(SecurityConstants.TOKEN_HEADER, token);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
    }
}

授權過濾器

package com.springsecurity.security.filter;

import com.springsecurity.exception.UserNameNotFoundException;
import com.springsecurity.security.constants.SecurityConstants;
import com.springsecurity.security.service.UserDetailsServiceImpl;
import com.springsecurity.security.utils.JwtTokenUtils;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.SignatureException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

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.logging.Logger;

/**
 * 過濾器處理所有HTTP請求,並檢查是否存在帶有正確令牌的Authorization標頭。例如,如果令牌未過期或簽名金鑰正確。
 */
@Slf4j
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    private final UserDetailsServiceImpl userDetailsService;

    private static final Logger logger = Logger.getLogger(JwtAuthorizationFilter.class.getName());

    public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserDetailsServiceImpl userDetailsService) {
        super(authenticationManager);
        this.userDetailsService = userDetailsService;
    }

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

        String token = request.getHeader(SecurityConstants.TOKEN_HEADER);
        if (token == null || !token.startsWith(SecurityConstants.TOKEN_PREFIX)) {
            SecurityContextHolder.clearContext();
        } else {
            UsernamePasswordAuthenticationToken authentication = getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

    /**
     * 獲取使用者認證資訊 Authentication
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String authorization) {
        log.info("get authentication");
        String token = authorization.replace(SecurityConstants.TOKEN_PREFIX, "");
        try {
            String username = JwtTokenUtils.getUsernameByToken(token);
            logger.info("checking username:" + username);
            if (!StringUtils.isEmpty(username)) {
                // 這裡我們是又從資料庫拿了一遍,避免使用者的角色資訊有變
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities());
                return userDetails.isEnabled() ? usernamePasswordAuthenticationToken : null;
            }
        } catch (UserNameNotFoundException | SignatureException | ExpiredJwtException | MalformedJwtException | IllegalArgumentException exception) {
            logger.warning("Request to parse JWT with invalid signature . Detail : " + exception.getMessage());
        }
        return null;
    }
}

三、效果

使用者登入

在這裡插入圖片描述

未登入訪問使用者介面

在這裡插入圖片描述

使用者訪問使用者介面

在這裡插入圖片描述

使用者訪問管理員介面

在這裡插入圖片描述

管理員登入

在這裡插入圖片描述

未登入訪問管理員介面

在這裡插入圖片描述

管理員訪問使用者介面

在這裡插入圖片描述

管理員訪問管理員介面

在這裡插入圖片描述

相關文章