Security——Token認證


主要就講解一下,如何使用Security實現token認證,順帶也講講默認的登錄,是如何實現的。

(helloworld級別的樣例代碼,可運行,仍然需要思考如何應用於實戰)

登錄流程

簡單的登錄流程如下:filter攔截登錄請求;provider驗證密碼以及其它信息;驗證成功走success回調,失敗走failure回調。

 

登錄成功之后的操作:

1、如果是token認證,成功之后需要寫回token,之后客戶端的每一個請求,都需要攜帶token,此外,還需要一個獨立的filter,攔截所有的請求,判斷token是不是有效的。

2、如果是session,那就往session中存儲用戶信息。

(Provider 集中了大部分的鑒權邏輯,默認實現可以看:DaoAuthenticationProvider)

 

 

AuthenticationToken(登錄令牌)

使用 Security 登錄時,需要將用戶信息封裝成 Authentication,Authentication 包含了登錄所需的關鍵參數,整個認證流程都會有 Authentication 的參與。


1、UsernamePasswordAuthenticationToken 是 Authentication的子類,源碼中這類名稱很多,實際是一個東西;
2、這個對象包含了用戶的賬號、密碼,以及其它登錄所需的的信息;
3、這個對象是有狀態變化的,“未認證的” 和 “已經完成認證的”(實際上說的是 setAuthenticated(false) 函數,詳見 UsernamePasswordAuthenticationToken 源碼);
4、設計Authentication類的時候,盡量避免使用繼承的寫法(Provider 是根據 Authentication 的 class進行區分的,寫完要監測注意執行效果)。

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
 * 直接復制{@link UsernamePasswordAuthenticationToken}全部源碼,根據自己的需求進行代碼擴展;
 * 注意{@link UsernamePasswordAuthenticationToken}的構造函數,2個參數的和3個參數的,效果是不一樣的;
 * 因為每一個Token都有對應的Provider,最好避免采用繼承的方式寫Token。
 *
 * @author Mr.css
 * @date 2021-12-23 10:51
 */
public class AuthenticationToken extends AbstractAuthenticationToken {
	//這里省略全部代碼
}

AbstractAuthenticationProcessingFilter(鑒權處理攔截)

執行的優先級非常高,即使沒有配置 Sevlet 或者 Controller,代碼也可以執行。
代碼執行結束,需要返回已經認證完畢 Authentication,如果認證成功,繼續走成功的回調接口,如果認證失敗,就走失敗的接口。

1、想看 Security 默認的功能實現,可以參考 UsernamePasswordAuthenticationFilter 代碼;
2、主要功能:攔截登錄請求,發起認證,最終返回鑒權結束的 Authentication;
3、代碼不會自動調用 Provider,需要手動執行 super.getAuthenticationManager().authenticate(authentication) 函數。

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 登錄驗證
 *
 * @author Mr.css
 * @date 2021-12-23 11:41
 */
public class LoginFilter extends AbstractAuthenticationProcessingFilter {

    LoginFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    /**
     * 嘗試認證,獲取request中的數據,發起認證
     *
     * @param request  -
     * @param response -
     * @return returning a fully populated Authentication object (including granted authorities)
     * @throws AuthenticationException -
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String userName = request.getParameter("userName");
        String pwd = request.getParameter("pwd");
        System.out.println("結果過濾器攔截...");
        AuthenticationToken authentication = new AuthenticationToken(userName, pwd);

        //發起認證,經過程序流轉,最終會到達Provider
        return super.getAuthenticationManager().authenticate(authentication);
    }
}

AuthenticationProvider

Provider 包含兩個主要功能,一個是查詢,一個就是認證,找到用戶的詳細信息,然后證明用戶的賬號、密碼都是有效的。

這個類包含兩個函數:
supports(Class<?> authentication):用於說明當前的 Provider 可以解析哪些 Authentication;
authenticate(Authentication authentication):認證用戶信息,參數與返回值一致,完成鑒權結束,要調整 Authentication 的狀態。

import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

/**
 * 用戶認證
 *
 * @author Mr.css
 * @date 2021-12-23 10:53
 */
public class AuthenticationProvider extends DaoAuthenticationProvider {

    /**
     * 標明當前Provider能夠處理的Token類型
     *
     * @param authentication tokenClass
     * @return boolean
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return AuthenticationToken.class == authentication;
    }

    /**
     * 身份鑒權
     *
     * @param authentication 身份證明
     * @return Authentication  已經完成的身份證明(a fully authenticated object including credentials)
     * @throws AuthenticationException e
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        System.out.println("身份認證:" + authentication);
        return authentication;
    }
}

UserDetailsService

相當於DAO,主要就是負責用戶身份信息查詢,包括密碼、權限,下面代碼是生產環境直接扣出來的,提供參考代碼,按需調整。 

import cn.seaboot.admin.user.bean.entity.Role;
import cn.seaboot.admin.user.bean.entity.User;
import cn.seaboot.admin.user.bean.entity.UserGroup;
import cn.seaboot.admin.user.service.PermService;
import cn.seaboot.admin.user.service.RoleService;
import cn.seaboot.admin.user.service.UserGroupService;
import cn.seaboot.admin.user.service.UserService;
import cn.seaboot.common.core.CommonUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * 查詢用戶詳細信息
 *
 * @author Mr.css
 * @date 2020-05-08 0:02
 */
@Configuration
public class CustomUserDetailsService implements UserDetailsService {

    @Resource
    private PasswordHelper passWordHelper;

    @Resource
    private UserService userService;

    @Resource
    private RoleService roleService;

    @Resource
    private UserGroupService userGroupService;

    @Resource
    private PermService permService;

    /**
     * 因為security自身的設計原因,角色權限前面需要添加ROLE前綴
     */
    private static final String ROLE_PREFIX = "ROLE_";
    /**
     * 默認添加一個權限,名稱為登錄,標明必須登錄才能訪問(個性化設計:只是為了方便組織代碼邏輯)
     */
    private static final String ROLE_LOGIN = "ROLE_LOGIN";

    /**
     * 因為security自身的設計原因,我們在用戶分組和角色權限,增加ROLE前綴
     *
     * @param role 角色
     * @return SimpleGrantedAuthority
     */
    private SimpleGrantedAuthority genSimpleGrantedAuthority(String role) {
        if (!role.startsWith(ROLE_PREFIX)) {
            role = ROLE_PREFIX + role;
        }
        return new SimpleGrantedAuthority(role);
    }

    /**
     * 用戶登錄並賦予權限
     *
     * @param userName 用戶帳號
     * @return UserDetails 用戶詳細信息
     * @throws UsernameNotFoundException 拋出具體的異常
     */
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        System.out.println("登錄的用戶是:" + userName);
        User user = userService.queryByUserCode(userName);
        UserGroup userGroup = userGroupService.queryById(user.getOrgId(), user.getGroupId());
        Role sysRole = roleService.queryById(user.getOrgId(), userGroup.getRoleId());
        //用戶權限列表
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        //添加用戶組權限
        if (CommonUtils.isNotEmpty(userGroup.getCode())) {
            String role = userGroup.getCode();
            grantedAuthorities.add(this.genSimpleGrantedAuthority(role));
        }
        //添加角色權限
        if (CommonUtils.isNotEmpty(sysRole.getRoleCode())) {
            String role = sysRole.getRoleCode();
            grantedAuthorities.add(this.genSimpleGrantedAuthority(role));
        }
        //添加普通權限
        Set<String> perms = permService.selectConcisePermsByRoleId(userGroup.getRoleId());
        for (String perm : perms) {
            if (CommonUtils.isNotEmpty(perm)) {
                grantedAuthorities.add(new SimpleGrantedAuthority(perm));
            }
        }
		
		// TODO: 測試時可以刪除上面其它權限配置,這里僅提供參考
        grantedAuthorities.add(new SimpleGrantedAuthority(ROLE_LOGIN));
        // TODO:獲取BCrypt加密的密碼,按需調整,這里我用的是自己的加密算法,可以直接使用BCryptPasswordEncoder
        String bCryptPassword = passWordHelper.getBCryptPassword(user.getPassword(), user.getPasswordSalt());
        return new org.springframework.security.core.userdetails.User(user.getUserCode(), bCryptPassword, grantedAuthorities);
    }
}

AuthenticationSuccessHandler

身份認證成功回調函數,如果普通登錄,就進行頁面轉發,如果是token認證,就向客戶端寫回一個token。

import cn.seaboot.admin.mvc.Result;
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;

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

/**
 * 身份認證成功
 *
 * @author Mr.css
 * @date 2021-12-23 11:59
 */
public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {


    /**
     * Called when a user has been successfully authenticated.
     * 認證成功之后調用
     *
     * @param request        -
     * @param response       -
     * @param authentication 認證信息
     * @throws IOException -from write
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        System.out.println("身份認證成功:" + authentication);
        //TODO: 登錄成功,將token寫回客戶端
        response.getWriter().write(JSON.toJSONString(Result.succeed()));
    }
}

 AuthenticationFailureHandler

在認證過程中,出現認證問題,需要拋出異常,在這里統一處理。

import cn.seaboot.admin.mvc.Result;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;

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

/**
 * 登錄失敗異常處理
 *
 * @author Mr.css
 * @date 2021-12-23 12:01
 */
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        Result result;
        if (exception instanceof BadCredentialsException ||
                exception instanceof UsernameNotFoundException) {
            result = Result.failed("賬戶名或者密碼輸入錯誤!");
        } else if (exception instanceof LockedException) {
            result = Result.failed("賬戶被鎖定,請聯系管理員!");
        } else if (exception instanceof CredentialsExpiredException) {
            result = Result.failed("密碼過期,請聯系管理員!");
        } else if (exception instanceof DisabledException) {
            result = Result.failed("賬戶被禁用,請聯系管理員!");
        } else {
            result = Result.failed("登陸失敗!");
        }
        response.getWriter().write(result.toString());
    }
}

TokenFilter

前面這些操作,只是完成了登錄流程,代碼還沒有完全結束。

不管是 session,還是使用 token,登錄成功之后,都需要一個獨立的filter,攔截所有的請求,證明你已經登陸過了。

如果是 token 認證,就需要驗證,你的每一個請求是否包含了 token,並且需要驗證 token 是否還有效。

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
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;
import java.util.ArrayList;
import java.util.List;

/**
 * 每一次請求都需要校驗一次token
 *
 * @author Mr.css
 * @date 2021-12-23 15:14
 */
public class TokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String token = httpServletRequest.getHeader("Authentication");
        System.out.println("token" + token);

        //授權
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
		//grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_LOGIN"));

        //正常設計,在LoginFilter那一步就必須創建Authentication,這里為了演示,創建一個虛擬的Authentication。
        AuthenticationToken authenticationToken = new AuthenticationToken("admin", "test", grantedAuthorities);
        authenticationToken.setDetails(new WebAuthenticationDetails(httpServletRequest));
		//添加到上下文中
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

		//未登錄直接拋出異常,交給spring異常切面統一處理,也可以自定義其它處理方式
        //throw new AccessDeniedException("登錄未授權!");
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

配置類

將上面這一堆代碼,組合起來,注冊進 spring 容器中

import cn.seaboot.admin.security.bean.entity.SecurityChain;
import cn.seaboot.common.core.CommonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;
import java.util.List;

/**
 * Security configuration
 *
 * @author Mr.css
 * @date 2020-05-07 23:38
 */
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    private Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);

    @Resource
    private CustomUserDetailsService customUserDetailsService;

    @Resource
    private BCryptPasswordEncoder passwordHelper;

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    /**
     * HttpSecurity相關配置
     *
     * @param http HttpSecurity
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
		
        //前面我們設置的登陸接口是/login,因此/login的配置是permitAll
        registry.antMatchers("/login").access("permitAll");

        // 添加攔截器
        LoginFilter loginFilter = new LoginFilter();
        TokenFilter tokenFilter = new TokenFilter();

        loginFilter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
        loginFilter.setAuthenticationFailureHandler(new LoginFailureHandler());

        loginFilter.setAuthenticationManager(super.authenticationManager());
        http.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterAfter(tokenFilter, LoginFilter.class);

        //禁用CSRF,默認用於防止CSRF攻擊的設置,模版引擎中使用
        http.csrf().disable();

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

        //禁用掉XFrameOptions,這個配置能讓IFrame無法嵌套我們的頁面,可以防止盜鏈,
        http.headers().frameOptions().disable();
    }

    /**
     * 設置用戶登錄和密碼加密功能
     *
     * @param auth AuthenticationManagerBuilder
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        AuthenticationProvider authenticationProvider = new AuthenticationProvider();
        authenticationProvider.setUserDetailsService(customUserDetailsService);
        authenticationProvider.setPasswordEncoder(passwordHelper);
        auth.authenticationProvider(authenticationProvider);
    }
}

 


免責聲明!

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



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