如何使用Spring Securiry實現先後端分離項目的登陸功能

若是不是先後端分離項目,使用SpringSecurity作登陸功能會很省心,只要簡單的幾項配置,即可以輕鬆完成登陸成功失敗的處理,當訪問須要認證的頁面時,能夠自動重定向到登陸頁面。可是先後端分離的項目就不同了,不能直接由後臺處理,而是要向前端返回相應的json提示。前端

在本例的介紹中,主要解決了如下幾個問題:java

1.使用json格式數據進行登陸。
2.登陸成功或失敗處理返回json提示。
3.未登陸時訪問須要認證的url時,返回json提示。
4.session過時時返回json提示。

1、引入security依賴

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

2、編寫配置文件

package com.hanstrovsky.config;
...

/**
 * @author Hanstrovsky
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // security默認不支持註解的方式的權限控制,加上這個註解開啓
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final MyUserDetailsService myUserDetailsService;

    private final MyPasswordEncoder myPasswordEncoder;

    public WebSecurityConfig(MyUserDetailsService myUserDetailsService, MyPasswordEncoder myPasswordEncoder) {
        this.myUserDetailsService = myUserDetailsService;
        this.myPasswordEncoder = myPasswordEncoder;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 定義加密解密方式
        auth.userDetailsService(myUserDetailsService).passwordEncoder(myPasswordEncoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .httpBasic()
                // 訪問須要認證的url,進行json提示
                .and().exceptionHandling()
                .authenticationEntryPoint((req, resp, e) -> {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    FrontResult frontResult = FrontResult.init(FrontResult.LOGIN, "未登陸或登陸超時!");
                    out.write(new ObjectMapper().writeValueAsString(frontResult));
                    out.flush();
                    out.close();
                })
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()// 必須認證以後才能訪問
            
                .and()
                .formLogin()// 表單登陸
                .permitAll() // 和表單登陸相關的接口通通都直接經過
            
                .and()
                .logout().deleteCookies("JSESSIONID")// 註銷登陸,刪除cookie
           // 自定義註銷成功,返回json
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        FrontResult frontResult = FrontResult.init(FrontResult.SUCCEED, "註銷成功!");
                        out.write(new ObjectMapper().writeValueAsString(frontResult));
                        out.flush();
                        out.close();
                    }
                })
            
                .and()
                // session 超時返回json提示
                .sessionManagement()
                .maximumSessions(5).maxSessionsPreventsLogin(true)// 同一用戶最大同時在線數量5個,超出後阻止登陸
            // session 超時返回json提示
                .expiredSessionStrategy(new SessionInformationExpiredStrategy() {
                    @Override
                    public void onExpiredSessionDetected(
                            SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {
                        HttpServletResponse resp = sessionInformationExpiredEvent.getResponse();
                        // 返回提示
                        resp.setContentType("application/json;charset=utf-8");
                        PrintWriter out = resp.getWriter();
                        FrontResult frontResult = FrontResult.init(FrontResult.LOGIN, "登陸超時!");
                        out.write(new ObjectMapper().writeValueAsString(frontResult));
                        out.flush();
                        out.close();
                    }
                });
        //用重寫的Filter替換掉原有的UsernamePasswordAuthenticationFilter
        http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}

    //註冊自定義的UsernamePasswordAuthenticationFilter,使用json格式數據登陸
    @Bean
    CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
        CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
        // 自定義登陸成功或失敗 返回json提示
        filter.setAuthenticationSuccessHandler((req, resp, authentication) -> {
            resp.setContentType("application/json;charset=utf-8");
            PrintWriter out = resp.getWriter();
            FrontResult frontResult = FrontResult.init(FrontResult.SUCCEED, "登陸成功!");
            out.write(new ObjectMapper().writeValueAsString(frontResult));
            out.flush();
            out.close();
        });
        filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter out = resp.getWriter();
                String errorMessage = "登陸失敗";
                FrontResult frontResult = FrontResult.init(FrontResult.FAILED, errorMessage);
                out.write(new ObjectMapper().writeValueAsString(frontResult));
                out.flush();
                out.close();
            }
        });
        filter.setFilterProcessesUrl("/user/login");
        //重用WebSecurityConfigurerAdapter配置的AuthenticationManager,否則要本身組裝AuthenticationManager
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }
}

3、實現Json登陸的處理邏輯

security默認提供了Basic和表單兩種登陸方式,不支持Json格式的數據,須要對處理登陸的過濾器進行修改。這裏,咱們重寫了UsernamePasswordAuthenticationFilter的attemptAuthentication方法。web

package com.hanstrovsky.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
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;
import java.io.InputStream;
import java.util.Map;

/**
 * 自定義過濾器,重寫 attemptAuthentication方法,實現使用json格式的數據進行登陸
 *
 * @author Hanstrovsky
 */
@Slf4j
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
                || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
            ObjectMapper mapper = new ObjectMapper();
            UsernamePasswordAuthenticationToken authRequest = null;
            try (InputStream is = request.getInputStream()) {
                Map<String, String> authenticationBean = mapper.readValue(is, Map.class);
                String username = authenticationBean.get("username");
                String password = authenticationBean.get("password");
                authRequest = new UsernamePasswordAuthenticationToken(
                        username, password);
            } catch (IOException e) {
                e.printStackTrace();
                authRequest = new UsernamePasswordAuthenticationToken(
                        "", "");
            }
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);

        } else {
            // 保留原來的方法
            return super.attemptAuthentication(request, response);
        }
    }
}

4、實現UserDetailsService接口

這個接口是用來提供用戶名和密碼的,能夠經過查詢數據庫獲取用戶。本例直接在代碼中寫死。spring

package com.hanstrovsky.service;

import com.hanstrovsky.entity.MyUserDetails;
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.Repository;

/**
 * @author Hanstrovsky
 */
@Repository
public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {

        // 能夠在此處自定義從數據庫查詢用戶
        MyUserDetails myUserDetail = new MyUserDetails();
        myUserDetail.setUsername(username);
        myUserDetail.setPassword("123456");
        return myUserDetail;
    }
}

5、實現PasswordEncoder接口

自定義密碼的加密方式。數據庫

package com.hanstrovsky.util;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * 自定義的密碼加密方法,實現了PasswordEncoder接口
 *
 * @author Hanstrovsky
 */
@Component
public class MyPasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {
        //加密方法能夠根據本身的須要修改
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return encode(charSequence).equals(s);
    }
}

6、實現UserDetails接口

這個類是用來存儲登陸成功後的用戶數據,security提供了直接獲取用戶信息的接口json

package com.hanstrovsky.entity;

import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * 實現UserDetails,可自定義添加更多屬性
 *
 * @author Hanstrovsky
 */
@Getter
@Setter
@Component
public class MyUserDetails implements UserDetails {

    //登陸用戶名
    private String username;
    //登陸密碼
    private String password;

    private Collection<? extends GrantedAuthority> authorities;

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

    private boolean accountNonExpired = true;

    private boolean accountNonLocked = true;

    private boolean credentialsNonExpired = true;

    private boolean enabled = true;
}

以上,即可以實現先後端分離項目基本的登陸功能。後端

相關文章
相關標籤/搜索