1. 程式人生 > >Spring Boot後臺腳手架搭建 [五] Spring Security登入實現以及認證過程

Spring Boot後臺腳手架搭建 [五] Spring Security登入實現以及認證過程

Spring Security實現登入

spring security的配置

    SecurityConfig

@Override
protected void configure(HttpSecurity http) throws Exception {
//跨站請求偽造禁用
http.csrf().disable();
// 基於token,所以不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//不需要校驗的檔案
http.authorizeRequests()
.antMatchers(
"/", "/*.html", "/favicon.ico", "/css/**", "/js/**", "/fonts/**", "/layui/**", "/img/**", "/v2/api-docs/**", "/swagger-resources/**", "/webjars/**", "/pages/**", "/druid/**", "/statics/**") .permitAll().anyRequest().authenticated(); http.formLogin().loginPage("/login.html").loginProcessingUrl("/login") .successHandler(authenticationSuccessHandler
).failureHandler(authenticationFailureHandler).and() .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 解決不允許顯示在iframe的問題 http.headers().frameOptions().disable(); http.headers().cacheControl(); http.addFilterBefore(
tokenFilter, UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); }

    SecurityHandlerConfig(spring security處理器)

/**
 * 登陸成功,返回token
 *
 */
@Bean
public AuthenticationSuccessHandler loginSuccessHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Token token = tokenService.saveToken(loginUser);
ResponseUtil.responseJson(httpServletResponse, HttpStatus.OK.value(),token);
}
    };
}
/**
 * 登入失敗
 */
@Bean
public AuthenticationFailureHandler loginFailureHandler() {
return new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
String msg = null;
            if (e instanceof BadCredentialsException) {
msg = "密碼錯誤";
} else {
msg = e.getMessage();
}
ResponseInfo info = new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", msg);
ResponseUtil.responseJson(httpServletResponse, HttpStatus.UNAUTHORIZED.value(), info);
}
    };
}
/**
 * 未登入
 */
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseInfo info = new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", "請先登入");
ResponseUtil.responseJson(httpServletResponse, HttpStatus.UNAUTHORIZED.value(), info);
}
    };
}
/**
 * 退出處理
 */
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
ResponseInfo info = new ResponseInfo(HttpStatus.OK.value() + "", "退出成功");
String token = TokenFilter.getToken(httpServletRequest);
tokenService.deleteToken(token);
ResponseUtil.responseJson(httpServletResponse,HttpStatus.OK.value(),info);
}
    };
}

Token攔截器

@Component
public class TokenFilter extends OncePerRequestFilter {
private static final String TOKEN_KEY = "token";
    private static final Long MINUTES_10 = 10 * 60 * 1000L;
@Autowired
private TokenService tokenService;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = getToken(request);
        if (StringUtils.isBlank(token)) {
LoginUser loginUser = tokenService.getLoginUser(token);
            if (loginUser != null) {
loginUser = checkLoginTime(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
        }
filterChain.doFilter(request,response);
}
/**
     * 校驗過期時間
     * 過期時間與當前時間對比,臨近過期10分鐘的話,自動重新整理快取
     *
     * @return
*/
private LoginUser checkLoginTime(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MINUTES_10) {
String token = loginUser.getToken();
loginUser = (LoginUser) userDetailsService.loadUserByUsername(loginUser.getUsername());
loginUser.setToken(token);
tokenService.refresh(loginUser);
}
return loginUser;
}
/**
     * 根據引數或者header獲得token
     * @param request
* @return
*/
public static String getToken(HttpServletRequest request) {
String token = request.getParameter(TOKEN_KEY);
        if (StringUtils.isBlank(token)) {
token = request.getHeader(TOKEN_KEY);
}
return token;
}
}

對token進行驗證攔截

具體實現

前端呼叫登入方法

function login(obj) {
$(obj).attr("disabled", true);
var username = $.trim($('#username').val());
var password = $.trim($('#password').val());
if (username == "" || password == "") {
$("#info").html('使用者名稱或者密碼不能為空');
$(obj).attr("disabled", false);
} else {
$.ajax({
type : 'post',
url : '/login',
data : $("#login-form").serialize(),
success : function(data) {
localStorage.setItem("token", data.token);
location.href = '/index.html';
},
error : function(xhr, textStatus, errorThrown) {
var msg = xhr.responseText;
var response = JSON.parse(msg);
$("#info").html(response.message);
$(obj).attr("disabled", false);
}
      });
}
}

因為在這裡我們配置了登入請求這裡被攔截下來了。

http.formLogin().loginPage("/login.html").loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);

後經過

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

這個攔截器的attemptAuthentication方法獲取到前端的使用者名稱和密碼

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

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

該方法只支援post請求,獲取到使用者名稱密碼後構造未認證的UsernamePasswordAuthentication 後設置details然後通過AuthenticationManager(實際上為ProviderManager的authenticate方法)完成驗證

 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();
        Iterator var6 = this.getProviders().iterator();

        while(var6.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var6.next();
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try {
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (AccountStatusException var11) {
                    this.prepareException(var11, authentication);
                    throw var11;
                } catch (InternalAuthenticationServiceException var12) {
                    this.prepareException(var12, authentication);
                    throw var12;
                } catch (AuthenticationException var13) {
                    lastException = var13;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                result = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var9) {
                ;
            } catch (AuthenticationException var10) {
                lastException = var10;
            }
        }

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

            this.eventPublisher.publishAuthenticationSuccess(result);
            return result;
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            this.prepareException((AuthenticationException)lastException, authentication);
            throw lastException;
        }
    }

    該方法先迴圈遍歷provider 找到具體執行該認證的provider 然後複製details 然後由具體的provider來完成認證

    具體的驗證處理由DaoAuthenticationProvider的父類AbstractUserDetailsAuthenticationProvider的authenticate方法來完成

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
        String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;

            try {
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User '" + username + "' not found");
                if (this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }

                throw var6;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }

            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }

        this.postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }

        該方法先獲取到登入的使用者名稱,如果快取中有UserDetails則從快取中獲取UserDetails如果沒有則根據使用者名稱和authentication獲取UserDetails 後進行一系列驗證成功後返回Authentication

驗證過程由DaoAuthenticationProvider的retrieveUser和additionalAuthenticationChecks方法來實現

 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }
 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

    這個方法通過loadUserByUsername來獲取到資料庫中的使用者資訊,所以我們要自己重寫實現UserDetailsService介面的loadUserByUsername方法

@Service
public class UserDetailsServiceImpl implements UserDetailsService{
@Autowired
private UserService userService;
@Autowired
private PermissionDao permissionDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userService.getUser(username);
        if (user == null) {
throw new AuthenticationCredentialsNotFoundException("使用者名稱不存在");
} else if (user.getStatus() == Status.LOCKED) {
throw new LockedException("使用者被鎖定,請聯絡管理員");
} else if (user.getStatus() == Status.DISABLED) {
throw new DisabledException("使用者已被封禁");
}
LoginUser loginUser = new LoginUser();
BeanUtils.copyProperties(user, loginUser);
List<Permission> permissions = permissionDao.listByUserId(user.getId());
loginUser.setPermissions(permissions);
        return loginUser;
}
}

通過loadUserByUsername獲取到資料庫的使用者資訊在通過上面的兩個方法和前端傳過來的使用者資訊進行比對就完成了登入認證。

認證完成後成功失敗等一系列處理在SecurityHandlerConfig進行處理,成功後將token儲存到redis中。

相關推薦

Spring Boot後臺腳手架搭建 [] Spring Security登入實現以及認證過程

Spring Security實現登入spring security的配置    SecurityConfig@Override protected void configure(HttpSecurity http) throws Exception { //跨站請求偽造禁用

spring boot後臺管理系統,spring security許可權控制

隨著spring boot的出現,java又上升了一個層次,以往tomcat部署war的形式也改變了,現在可以直接一個jar包、一行命令,真正實現一次編譯隨處執行的理念了。      閒暇之餘小威老師做了一個以spring boot為後臺,layui、bootstr

Spring Boot框架的搭建

簡化 output 項目依賴 boot jdk1 cat uil prop 自己 一、優點: 1.簡化了配置,是基於Spring4的一套快速開發整合包,減少復雜度 而Spring MVC基於Spring 的一個MVC框架 2.會有一個statrter整合包,減少樣板

spring boot開發環境搭建

oot osi jdbc 宋體 tid mes true err table 軟件151 朱實友 1.新建一個maven項目 Maven配置文件: <!-- Inherit defaults from Spr

Spring Boot的環境搭建

log main函數 ger warn 3.1 集合 font boot col 軟件152 陳卓 一、概念: 從最根本上來講,Spring Boot就是一些庫的集合,它能夠被任意項目的構建系統所使用。簡便起見,該框架也提供了命令行界面,它可以用來運行和測試Boot

spring boot hello world 搭建

-i ref quest img turn stat oid lease void 1.下載地址: Eclipse:http://www.eclipse.org/downloads/packages/eclipse-ide-java-ee-developers/neon

Spring Boot項目搭建

pri resources 以及 urn ext tar tom 管理 tex 1.Spring Boot概述 Spring Boot是由Pivotal團隊提供的全新框架,其設計目的是用來簡化新Spring應用的初始搭建以及開發過程。該框架使用了特定的方式來進行配

Spring Boot參考教程(Spring Boot配置使用之配置類用法

expr web程序 成功 驗證 pan hub parameter lan fix 4.2. SpringBoot配置使用之配置類使用 Spring Boot的大部分自動配置都可以滿足應用要求,但如果想精確的控制應用,或者想覆蓋自動配置,使用配置類是另一種很好的選擇,強調

Spring Boot實戰筆記()-- Spring高級話題(Spring Aware)

ktr mea 框架 .com cat 分享 aware war uic 一、Spring Aware   Spring 依賴註入的最大亮點就是你所有的 Bean 對 Spring容器的存在是沒有意識的。即你可以將你的容器替換成其他的容器,如Google Guice,這時

Spring Boot快速入門():使用MyBatis(註解形式)進行數據庫操作

訪問 ins name ont clas assert xxx main apach 原文地址:https://lierabbit.cn/articles/7 添加依賴 新建項目選擇web,MyBatis,MySQL三個依賴 對於已存在的項目可以在bulid.gradle

Eclipse對spring-bootspring-boot-mybatis的搭建

安裝 OS arch true password username 數據 nbsp maven 1.準備工作 1.1.如果沒有sts(spring tool suite)插件, 則需要下載。 1.1.1.eclipse下載的話,一定要註意版本,因為eclipse會直接下載

Spring Boot 框架的搭建方法(手記)

基本 企業 spring 配置 turn 處理器 control 默認 文件 能夠 一.spring boot歷史背景 Spring 誕生時是 Java 企業版(Java Enterprise Edition,JEE,也稱 J2EE)的 輕量級代替品。無需開發重量級的 En

Spring Boot使用AOP搭建統一處理請求日誌和使用log4j記錄不同級別的日誌

受http://blog.didispace.com/springbootaoplog/啟發,今天給Spring Boot專案搭建了統一處理請求日誌的切面並引入log4j記錄不同層級日誌。 mark一下這個過程,以及原文中沒有涉及到的一些疑問 一.  新增要使用的依賴&nbs

Spring Boot學習筆記()—— 使用JUnit5編寫單元測試

1.開發環境 Mac OS 10.14 JDK8 Maven 3.5.3 Eclipse 4.9.0 Spring Boot 2.0.5.RELEASE JUnit 5.1.1 2.JUnit5簡介[1] JUnit 5跟以前的JUni

簡易Spring-boot專案的搭建demo

最近剛開始學習Spring-boot框架,從最開始搭建一個Maven工程--配置檔案--測試demo--啟動Spring—boot專案。 1.新建一個maven project(Create a simple project)   2.Next   

Spring-Boot web專案搭建

Spring-Boot快速搭建web專案詳細總結 最近在學習Spring Boot 相關的技術,剛接觸就有種相見恨晚的感覺,因為用spring boot進行專案的搭建是在太方便了,我們往往只需要很簡單的幾步,便可完成一個spring MVC專案的搭建,感覺就是下圖:  好,下

Spring Boot環境的搭建

什麼是Spring Boot? Spring Boot是由Pivotal團隊提供的全新框架,其設計目的是用來簡化新Spring應用的初始搭建以及開發過程。該框架使用了特定的方式來進行配置,從而使開發人員不再需要定義樣板化的配置。 自己的理解就是spring boot其實不是什麼新

spring boot 專案重新搭建----------整合Redis

1.匯入依賴 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artif

spring boot 專案重新搭建----------定時任務、事件監聽

1.開啟定時任務 . [email protected]設定定時時間規則 3.有時候需要程式啟動就進行執行的操作可用事件監聽來實現 監聽ContextRefreshedEvent事件,當所有的bean都初始化完成並被成功裝載後會觸發該事件,實現Applicati

spring boot 專案重新搭建----------mvc配置:引數解析

7.addResourceHandlers靜態資源解析 如: registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); 8.addC