spring-security-結合JWT的簡單demo


github源碼地址

spring-security-demo

前言:本來是想盡量簡單簡單點的寫一個demo的,但是spring-security實在是內容有點多,寫着寫着看起來就沒那么簡單了,想入門spring-security的話還是需要下些功夫的,這遠沒有Mybatis、JPA之類的容易入門
一個spring-security采用jwt認證機制的demo。
以下代碼僅為說明代碼作用,有的並不完整,如若要參考請git clone整個項目代碼查看
參考:
spring security學習(SpringBoot2.1.5版本)
SpringBootSecurity學習(13)前后端分離版之JWT
重拾后端之Spring Boot(四):使用JWT和Spring Security保護REST API

spring-security

config.securityConfig是springSecurity的安全配置類,在這個類中配置需要驗證的接口、需要放行的接口,配置登錄成功失敗的處理器

1.最簡單的用戶角色權限控制demo

最簡單是demo是直接在securityConfig中配置存在內存中的用戶對象,可以采用一下代碼配置用戶角色:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("user").password({noop}123).roles("USER")
                .and()
                .withUser("admin").password({noop}123).roles("ADMIN")
                .and()
                .withUser("one").password({noop}123).roles("ONE")
                .and()
                .withUser("two").password({noop}123).roles("TWO");
    }

然后在securityConfig加注解開啟接口的preAuth注解支持

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true,jsr250Enabled = true)
public class securityConfig extends WebSecurityConfigurerAdapter {

然后可以直接在Controller的接口上加注解

    /* 只有角色ONE才能訪問 */
    @PreAuthorize("hasRole('ONE')")
    @GetMapping("/hello")
    public String hello(){
        return "hello Spring Security";
    }

然后訪問localhost:8080/two,發現會跳轉到login登錄頁面,此時以one登錄進去可以正常訪問,但是以其它角色訪問均會出錯。至此,最簡單的demo已完成。

2.修改用戶為數據庫用戶

上面的用戶是存在內存中的,接下來需要將其改為從數據庫中獲取用戶信息並驗證。
首先需要在securityConfig中配置spring-security加載用戶時使用的類,spring-security通過我們提供的這個類得到一個用戶信息,該用戶信息中一般包含用戶名、密碼、角色,spring-security得到這些信息后完成后續操作。
提供該類給securityConfig

    @Qualifier("userDetailServiceImpl")
    @Autowired
    private UserDetailsService userDetailsService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService) // 提供給spring-security的類
                .passwordEncoder( new BCryptPasswordEncoder() );  // 這是密碼加密的類,可以理解為將明文密碼加密成hash值,可以先忽略照寫
    }

然后需要實現這個類

@Component
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private UserPasswordRepository userPasswordRepository;
    @Autowired
    private UserRoleRepository userRoleRepository;

    /**
     * 我的數據庫表分為User表、UserInfo用戶詳細信息表、UserPassword密碼表、UserRole用戶角色表
     * spring-security會給這個方法提供一個用戶名,然后我們實現根據用戶名得到這個用戶的UserDetail信息(類似於包含用戶名、密碼、角色的實體類,下一步重寫它)
     * 然后返回的就是這個UserDetail,spring-security可以使用該類完成其它的操作
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findFirstByUsername(username);
        Integer id = user.getId();
        if (Objects.nonNull(user) && username.trim().length() <= 0) {
            throw new UsernameNotFoundException("用戶名錯誤");
        }
        // 填充所有角色信息
        List<GrantedAuthorityImpl> grantedAuthorities = new ArrayList<>();
        List<UserRole> roles = userRoleRepository.findByCreator_Id(id);
        for (UserRole role : roles) {
            grantedAuthorities.add(new GrantedAuthorityImpl("ROLE_" + role.getRole()));
        }
        return new UserDetailImpl(
                    username,
                    userPasswordRepository.findByCreator_Id(id).getPassword(),
                    grantedAuthorities
                );
    }
}

實現UserDetail,這個類就像是一個實體類,但是實現了UserDetails接口,遵循spring-security的規范以讓spring-security能使用它

@NoArgsConstructor
@ToString
public class UserDetailImpl implements UserDetails {

    private String username;

    @JsonIgnore
    private String password;

    private List<GrantedAuthorityImpl> authorities;

    @JsonIgnore
    private boolean accountNonExpired;
    @JsonIgnore
    private boolean accountNonLocked;
    @JsonIgnore
    private boolean credentialsNonExpired;
    @JsonIgnore
    private boolean enabled;

    public UserDetailImpl(String username, String password, List<GrantedAuthorityImpl> authorities) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
        this.accountNonExpired = true;
        this.accountNonLocked = true;
        this.credentialsNonExpired = true;
        this.enabled = true;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

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

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

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

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

最好再實現一下GrantedAuthority,這個是角色信息的規范接口

@NoArgsConstructor
public class GrantedAuthorityImpl implements GrantedAuthority {
    private String authority;

    public GrantedAuthorityImpl(String authority) {
        this.authority = authority;
    }

    @Override
    public String getAuthority() {
        return authority;
    }
}

上述這些就完成了spring-security用戶表轉移到數據庫的操作了。

3.引入jwt

上述過程中,spring-security默認使用session-cookie的方法保存一個連接中的用戶信息,然后拿這些用戶信息到數據庫查詢。接下來可以改造成為jwt保存用戶信息,jwt其實就是平時經常看到的token保存用戶信息,其機制是直接將用戶信息寫在token中,然后就這個token進行簽名后頒發給用戶,用戶發起請求時可以攜帶token,服務器就可以直接給用戶認證信息了。
首先我們先來構造jwt token。
首先是jwt的工具類,該類提供信息HMACSHA256加密、信息簽名、測試token是否合法

public class JWTUtils {
    public static final String DEFAULT_HEADER = "\"alg\":\"HS256\",\"typ\":\"JWT\"";

    public static final String SECRET = "woshizengchunmiao";

    public static final long EXPIRE_TIME = 1000 * 60 * 60 * 24;

    public static final String HEADER_TOKEN_NAME = "Authrization";

    public static String encode(String input) {
        return Base64.getEncoder().encodeToString(input.getBytes());
    }

    public static String decode(String input) {
        return new String(Base64.getDecoder().decode(input));
    }

    public static String HMACSHA256(String data, String secret) throws Exception {
        Mac hmacSHA256 = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256");
        hmacSHA256.init(secretKeySpec);
        byte[] bytes = hmacSHA256.doFinal(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte aByte : bytes) {
            sb.append(Integer.toHexString((aByte & 0xFF) | 0x100), 1, 3);
        }
        return sb.toString().toUpperCase();
    }

    public static String getSignature(String payload) throws Exception {
        return HMACSHA256(encode(DEFAULT_HEADER) + encode(payload), SECRET);
    }

    public static String testJwt(String jwt) {
        String[] split = jwt.split("\\.");
        try {
            if (!(HMACSHA256(split[0] + split[1], SECRET).equals(split[2]))) {
                return null;
            }
            if (!decode(split[0]).equals(DEFAULT_HEADER)) {
                return null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return decode(split[1]);
    }
}

然后提供一個JWT類,構造該類時,只需要將想放在token上的信息傳入構造函數,即可得到一個想要的JWT,調用toString方法就得到了token

public class JWT {
    private String header;

    private String payload;

    private String signature;

    public JWT(String payload) throws Exception {
        this.payload = JWTUtils.encode(payload);
        this.header = JWTUtils.encode(JWTUtils.DEFAULT_HEADER);
        this.signature = JWTUtils.getSignature(payload);
    }

    @Override
    public String toString() {
        return header + "." + payload + "." + signature;
    }
}

4.jwt設置到spring-security

以上兩個類就完成了token的構造,然后我們需要用它來代替spring-security中的session-cookie機制。首先需要將spring-security的session關閉,實質上我的理解是,token是一個虛擬的session,每次建立連接時,spring-security將它解析出來把它作為認證信息放到Holder里。
關閉session,在securityConfig

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 設置無session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    }

然后需要寫一個Filter,在spring-security進行用戶名-密碼驗證前搶先發生,對token進行驗證,若token合法就放入認證信息,就完成了安全認證;若token不合法直接失敗。
先配置這個Filter到config中

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 攔截登錄請求
                .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

然后實現這個Filter

/**
 * 驗證token是否正確,並從token中還原"session"信息
 */
public class JwtAuthenticationFilter extends GenericFilter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String token = request.getHeader(JWTUtils.HEADER_TOKEN_NAME);   // 從請求頭中拿到token
        if (Objects.nonNull(token) && token.trim().length() > 0) {
            String payload = JWTUtils.testJwt(token);   // 從token中拿到payload
            if (Objects.nonNull(payload) && payload.trim().length() > 0) {
                ObjectMapper objectMapper = new ObjectMapper();
                // 我這個項目的payload是UserDetailImp的序列化后的Json,這里將其還原為UserDetailImpl對象
                UserDetailImpl user = objectMapper.readValue(payload, UserDetailImpl.class);   
                // 將還原得到的認證信息交給spring-security管理(用戶信息,認證,用戶角色表)
                SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities()));
            }
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

以上,就完成了spring-security使用JWT的全部過程。可以測試使用了

為了方便測試,我還提供了SuccessHandle、FailureHandle、AccessDeniedHandlerImpl用於spring-security登錄成功、登錄失敗、沒有認證信息的處理器,其中,SuccessHandle在登錄成功后返回當前認證信息的token,拿這個token放到請求頭訪問接口時,即可自動完成認證。

測試

提供了一個hello的Controller層接口

@RestController
public class hello {
    // 擁有ADMIN角色才可以訪問
    @PreAuthorize("hasAnyRole('ADMIN')")
    @RequestMapping("/hello")
    String test() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication.getPrincipal() instanceof UserDetailImpl) {
            UserDetailImpl user = (UserDetailImpl) authentication.getPrincipal();
            System.out.println(user.getUsername());
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                System.out.println(authority.getAuthority());
            }
        }
        return "hello";
    }
}

直接訪問該接口,由於沒有認證,會跳轉到/login接口下
image
以admin-123登錄后跳轉成功頁面並得到token
image
復制token,放到postman的header里,然后再次請求/hello
image
發現成功得到響應
image
到控制台看看,得到用戶名和角色名
image

換user-123角色的token登錄看看
image
image

還可以更換root角色,不再贅述。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM