👍SpringSecurity單體項目最佳實踐

SpringSecurity單體項目最佳實踐

到這裡,我們的SpringSecurity就已經完結啦,文章中可能有些地方不能做到全面覆蓋,視頻教程地址

1、搭建環境

  • 建議下載初始項目,跟着文章一步一步搭建。加深對於SpringSecurity的理解。

  • ❌ 需要將application.properties的數據庫配置,改成您自己對應的信息

  • ❌ 如若依賴問題,修改Idea Maven,改成自己的

  • ❌ 還需將Jdk版本改成您自己所使用的的版本。項目使用的是JDK12

  • ❌ 數據庫腳本在完成項目中的sql文件中

2、簡單使用

  • 添加SpringSecurity依賴

  • ❌ 註:這裡沒有申明版本號,是由於我們項目繼承的SpringBoot父項目,它已經為我們適配了對於的版本。

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

然後啟動項目即可:

  • 🅰️ 觀察控制台:這是SpringSecurity為我們臨時生成的密碼,默認用戶名為user

image-20221110224337763

  • 🅱️ 回到瀏覽器,輸入//localhost:8080/community,由於我們此時還未登陸,會重定向到默認創建的登陸頁面中,這是SpringSecurity默認為我們做的。
  • 🕐 輸入控制台的密碼,即可進入到系統,

image-20221110224606085

3、自定義使用

  • 相信小夥伴們已經對SpringSecurity已經有了初步的了解,但是正常的項目中,不可能採用這個默認登陸頁面呀,這點SpringSecurity也早就想到了。
  • 當然可以自定義登陸頁面,但是在自定義登陸頁面之前,我們需要簡單處理一下我們的實體類。

在用戶登錄時,系統會根據用戶名,從存儲設備查找該用戶的密碼及權限等,將其組裝成一個UserDetails對象。並用UserDetails中的數據對用戶進行認證,決定其輸入的用戶名/密碼是否正確。

  • 🔻 觀察UserDetails結構
public interface UserDetails extends Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();//權限 
	String getPassword();    //密碼
	String getUsername();    //用戶名
 
	boolean isAccountNonExpired();   //賬號是否未過期
	boolean isAccountNonLocked();    //賬號是否未鎖定
	boolean isCredentialsNonExpired();//密碼是否未過期
	boolean isEnabled();        //是否激活
  • 裏面定義了許多關於用戶的信息,可以看到它是一個接口,並不能直接使用。那麼肯定就有默認的實現類,要不然我們上面的登陸功能是怎麼完成的呢。
  • 🅿️ 此項目中採用 實體類繼承它的方式來完成。
@Data
public class User implements UserDetails {

    private int id;
    private String username;
    private String password;
    private String salt;
    private String email;
    private int type; // 1 管理員 2普通用戶
    private int status;
    private String activationCode;
    private String headerUrl;
    private Date createTime;

    //權限
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> permissions = new ArrayList<>();
        permissions.add((GrantedAuthority) () -> {
            switch (type) { // 
                case 1:
                    return "ADMIN";
                default:
                    return "USER";
            }
        });
        return permissions;
    }

    // true 帳戶未過期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // true 帳戶未鎖定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // true 憑證未過期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // true 賬號是否可用
    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • ⁉️ 定義了UserDetails之後,當然還遠遠不夠,哪個方法查詢數據庫來獲取我們的用戶信息呢?就是Security中的UserDetailsService接口

image-20221110230218289

  • ⭕️ 它肯定也有默認實現類的,但是我們需要查詢數據庫對應的用戶數據,所以我們還是採用自定義的方式去完成。
@Service
public class UserService implements UserDetailsService {
	
    @Resource
    private UserMapper userMapper;
	
    // 根據用戶名去查詢用戶數據
    public User findUserByName(String username) {
        return userMapper.selectByName(username);
    }
	
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return this.findUserByName(username);
    }
}
  • ⛎ 完成到這裡,對於用戶信息的功能已經實現,但是我們還沒有配置我們的登陸界面。

配置Security

  • ❗️ 在config目錄下創建SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Resource
    private UserService userService;
}
  • ❗️ 正常項目中,肯定會有許多的靜態資源,這些都可以在不登錄的情況下訪問,如css、js等
    @Override
    public void configure(WebSecurity web) throws Exception {
        // 忽略靜態資源
        web.ignoring().antMatchers("/resources/**");
    }
  • ❗️ 當然我們上面的UserService只實現了認證的查詢,並沒有配置在何時去調用這個類。

認證規則二選其一即可

    // AuthenticationManager: 認證的核心接口
    // AuthenticationManagerBuilder: 用戶構建AuthenticationManager對象的工廠類
    // ProviderManager: AuthenticationManager默認使用的實現類
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //內置的認證規則
		//auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());

        // 自定義認證規則
        // AuthenticationProvider: ProviderManager持有一組AuthenticationProvider,每個AuthenticationProvider負責一種認證
        // 委託模式:
        // AuthenticationProvider: 就好比登陸方式,不僅有密碼登錄,且還有微信,等其他登陸方式,每一種登陸方式對應一個AuthenticationProvider
        auth.authenticationProvider(new AuthenticationProvider() {
            // Authentication: 用於封裝認證信息的接口,不同實現類代表不同類型的認證信息
            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                String username = authentication.getName();
                String password = authentication.getCredentials().toString();

                User user = userService.findUserByName(username);
                if (user == null) {
                    throw new UsernameNotFoundException("賬號或密碼錯誤!");
                }

                password = CommunityUtil.md5(password + user.getSalt());
                if (!user.getPassword().equals(password)) {
                    throw new BadCredentialsException("賬號或密碼錯誤!");
                }
                // principal:認證的主要信息 credentials:代表用戶 authorities:權限信息
                return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
            }

            // 當前的AuthenticationProvider 支持哪種類型的認證。
            @Override
            public boolean supports(Class<?> aClass) {
                // UsernamePasswordAuthenticationToken: Authentication接口常用的實現類
                // 這樣配置,我們當前項目只支持UsernamePasswordAuthenticationToken的認證
                return UsernamePasswordAuthenticationToken.class.equals(aClass);
            }
        });
    }
  • ❗️ 配置了以上步驟,是不是覺得Security挺麻煩的,別急馬上到頭了。
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 登陸相關配置
    http.formLogin()
            .loginPage("/loginpage") // 登陸頁面
            .loginProcessingUrl("/login") // 處理登陸請求的路徑
            .successHandler(new AuthenticationSuccessHandler() { // 認證成功處理器
                @Override
                public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                    // 重定向到主頁面
                    response.sendRedirect(request.getContextPath() + "/index");
                }
            })
            .failureHandler(new AuthenticationFailureHandler() { // 認證失敗處理器
                @Override
                public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                    // 請求轉發到登陸頁面
                    // 因為在項目中,登陸失敗後,往往需要攜帶錯誤信息到頁面展示,所有採用請求轉發的方式
                    request.setAttribute("error", e.getMessage());
                    request.getRequestDispatcher("/loginpage").forward(request, response);
                }
            });
    // 退出相關配置
    http.logout().logoutUrl("/logout").logoutSuccessUrl("/index"); // 退出後重定向到的接口

    // 授權配置 配置什麼路徑只能什麼權限訪問
    http.authorizeRequests()
            .antMatchers("/letter").hasAnyAuthority("USER", "ADMIN")
            .antMatchers("/admin").hasAnyAuthority("ADMIN")
            .and().exceptionHandling().accessDeniedPage("/denied"); //無權限時,重回定向到的頁面
}
  • 現在對於後端的配置就完成啦,前端界面建議直接從完成的項目中copy
  • ❗️ 一定要檢查數據庫有沒有對應的用戶數據哦!!!
  • 接下來就是你們的時間啦。自行測試
  • 但是正常的項目中,登陸功能一定會有驗證碼的存在,SpringSecurity也想到了這一點,我們都知道SpringSecurity是由一大串過濾器來完成對應功能的,也就是說,我們需要在登陸校驗之前完成對於驗證碼的校驗。如下:
        // 增加Filter 處理驗證碼
        http.addFilterBefore(new Filter() {
            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest request = (HttpServletRequest) servletRequest;
                HttpServletResponse response = (HttpServletResponse) servletResponse;
                if (request.getServletPath().equals("/login")) {
                    // 正常項目中 驗證碼會存儲在Session或者Redis中,為了方便。此項目的驗證碼都是1234
                    String verifyCode = request.getParameter("verifyCode");
                    if (!"1234".equals(verifyCode)) {
                        request.setAttribute("error", "驗證碼錯誤!");
                        request.getRequestDispatcher("/loginpage").forward(request, response);
                        return;
                    }
                }
                // 放行請求,執行到下一個過濾器
                filterChain.doFilter(request, response);
            }
        }, UsernamePasswordAuthenticationFilter.class);

        // 記住我功能
        http.rememberMe()
                .tokenRepository(new InMemoryTokenRepositoryImpl()) // 用戶的令牌存儲到哪,InMemoryTokenRepositoryImpl 存儲到內存中
                .tokenValiditySeconds(3600 * 24) // 過期時間
                .userDetailsService(userService);// 當關閉瀏覽器後,第二次訪問,去拿重新查詢用戶的數據

到這裡,我們的SpringSecurity就已經完結啦,文章中可能有些地方不能做到全面覆蓋,視頻教程地址