1. 程式人生 > 實用技巧 >springboot+springsecurity+vue實現簡單的登陸認證

springboot+springsecurity+vue實現簡單的登陸認證

1、建立SecurityUser類,需要實現UserDetails介面

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

import java.io.Serializable;
import java.util.Collection;
import java.util.Date;

/**
 *
 * 封裝登陸使用者的資訊
 */
@Data
public class SecurityUser implements UserDetails {

    private String uid;

    private String username;

    private String password;

    private Integer sex;

    private String description;

    private Integer age;

    private String birthday;

    private Integer isDeleted;
    private Integer status;//1未禁用0禁用

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

    @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、編寫UserAuthenticationFilter過濾器,這裡需要繼承UsernamePasswordAuthenticationFilter

原因:

通過檢視UsernamePasswordAuthenticationFilter獲取使用者名稱和密碼的實現方法可以看到,預設只能獲取form表單提供的資料,無法獲得請求體中的資料。所以,要想獲得請求體中的資料,需要自定義過濾器。

這裡有兩種方式獲得使用者名稱和密碼

  • 直接重寫obtainPasswordobtainUsername
  • 檢視attemptAuthentication這個方法我們可以發現,使用者名稱和密碼是在這個方法裡面獲得並且使用的,因此我們可以直接重寫這個方法。(這裡使用的第二種方式)

import com.background.modules.security.bean.SecurityUser;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.web.authentication.UsernamePasswordAuthenticationFilter;

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

@Slf4j
public class UserAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            SecurityUser user = new ObjectMapper().readValue(req.getInputStream(), SecurityUser.class);
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage());
        }

    }
    
}

3、編寫UserDetailsServiceImpl,並且實現UserDetailsService

這裡只需要實現loadUserByUsername方法,驗證使用者是否存在、是否被禁用

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.background.modules.security.bean.SecurityUser;
import com.background.modules.user.bean.User;
import com.background.modules.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
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.Component;

@Component
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserService userService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getOne(new QueryWrapper<User>().eq("username", username));
        if(user==null){
            throw new BadCredentialsException("使用者不存在");
        }
        if(user.getStatus()==0){
            throw new BadCredentialsException("使用者被禁用");
        }
        SecurityUser userInfo = new SecurityUser();
        BeanUtils.copyProperties(user,userInfo);
        return userInfo;
    }
}

4、編寫UserLoginAuthenticationProvider,繼承DaoAuthenticationProvider

通過繼承DaoAuthenticationProvider,可以自定義使用者密碼驗證並檢視異常資訊。

具體效果

若不實現該類,丟擲的異常資訊會都變成Bad credentials

具體效果


import com.background.modules.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;


@Component
public class UserLoginAuthenticationProvider extends DaoAuthenticationProvider {

    @Autowired
    private UserDetailsServiceImpl detailsService;
    @Autowired
    private PasswordEncoder encoder;

    /**
     * 找到容器中的detailsService,並執行setUserDetailsService方法,完成賦值
     *
     * 必須要給UserDetailsService賦值,否則會出現UnsatisfiedDependencyException
     */
    @Autowired
    private void setDetailsService() {
        setUserDetailsService(detailsService);
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        String presentedPassword = authentication.getCredentials().toString();
        if (!encoder.matches(presentedPassword, userDetails.getPassword())) {
            throw new BadCredentialsException(messages.getMessage("badCredentials", "使用者密碼錯誤"));
        }
    }
}

5、編寫主配置類SecurityConfig,繼承自WebSecurityConfigurerAdapter

import com.alibaba.fastjson.JSON;
import com.background.modules.security.filter.UserAuthenticationFilter;
import com.background.modules.security.provider.UserLoginAuthenticationProvider;
import com.common.util.R;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * Created by X on 2020/7/22 16:30
 */
@Slf4j
@SpringBootConfiguration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserLoginAuthenticationProvider loginAuthenticationProvider;
    /**
     * 密碼加密
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 請求攔截、對映
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests(auth->{
            //開放swagger、登陸頁面的訪問許可權
           auth.antMatchers("/swagger-ui.html").permitAll() 
                   .antMatchers("/swagger-resources/**").permitAll()
                   .antMatchers("/webjars/**").permitAll()
                   .antMatchers("/v2/**").permitAll()
                   .antMatchers("/api/**").permitAll()
                   .antMatchers("/background/login").permitAll()
                   .anyRequest().authenticated();
        });
        //啟用自定義的過濾器
        http.addFilterAt(userAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        http.cors();//啟用跨域
        http.csrf().disable();//關閉跨站攻擊
    }

    /**
     * 使用者認證
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //使用自定義的Provider,進行資料校驗
        auth.authenticationProvider(loginAuthenticationProvider);
    }

    /**
     * 解決無法直接注入 AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManager() throws Exception
    {
        return super.authenticationManager();
    }
    /**
     * 自定義成功回撥、失敗回撥、登陸url地址等
     * 
     * 可以在自定義UserAuthenticationFilter裡面直接重寫對應方法,
     * 例 成功回撥:
     *      @Override
     *     public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
     *         super.setAuthenticationSuccessHandler(successHandler);
     *     }
     * @return
     * @throws Exception
     */ 
    @Bean
    public UserAuthenticationFilter userAuthenticationFilter() throws Exception {
        UserAuthenticationFilter filter = new UserAuthenticationFilter();
        //設定驗證成功後的回撥
        filter.setAuthenticationSuccessHandler((request,response,authentication)->{
            log.info("使用者認證成功");
            //響應成功狀態碼必須為200
            response.setStatus(HttpStatus.SC_OK);
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("utf-8");
            //將資料以json的形式返回給前臺
            response.getWriter().print(JSON.toJSONString(R.ok()));
        });
        //設定驗證失敗後的回撥
        filter.setAuthenticationFailureHandler((request,  response,  exception) ->{
            log.info("使用者認證失敗----{}",exception.getMessage());
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("utf-8");
            //將資料以json的形式返回給前臺
            response.getWriter().print(JSON.toJSONString(R.error(exception.getMessage())));
        });
        //設定使用者發起登陸請求時的url
        filter.setFilterProcessesUrl("/background/login");
        filter.setAuthenticationManager(authenticationManager());
        return filter;
    }
}

若沒有實現UserLoginAuthenticationProvider,這需要通過下面的方法實現資料校驗

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

6、vue開啟請求攜帶cookies

axios.defaults.withCredentials=true//開啟攜帶cookies