Spring Security

Spring Security 實現如下功能(摘要格式顯示太醜了放在下面)
    1》JDBC 數據庫認證                            (UserDetailsService) 
    2》系統權限控制                                  (GrantedAuthority)
    3》自定義登陸界面以及登陸提示           (MessageSource)
    4》登陸驗證碼認證                               (UsernamePasswordAuthenticationFilter)
    5》Session 使用 Redis 作存儲              (SecurityContext 實現)       css

 

        很久沒有寫博文了,放假閒在家裏整了整 Spring Security 的集羣Session沒有同步的問題,整了好幾天沒整好最後使用了一種比較 Low 的實現方式,這篇文章是給一些不懂Spring Security得朋友看的當作入門級的教程吧。html

        教程的運行流程圖,回頭補上。留下時間爲證 2018-4-7 13:53:30java

  上碼擼起 走你~ web

第一部分->用戶實現redis

        自定義用戶須要實現UserDetails 的接口,使用了@Data 還用Get Set 只是爲了UserName 不跟UserDetails 中的登陸名稱衝突spring

@Data
class   UserInfo implements UserDetails {

    public Log logger = LogFactory.getLog(UserInfo.class);

    private String userName;
    private String password;

    List<MyGrantedAuthority> list;  //用戶所擁有的權限 GrantedAuthority


    public UserInfo() {
    }

    public UserInfo(String userName, String password) {
        this.userName = userName;
        this.password = password;
    }

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

    @Override
    public String getPassword() {
        return this.userName;
    }

    @Override
    public String getUsername() {
        return this.password;
    }

    @Override
    public boolean isAccountNonExpired() {//指示用戶的賬戶是否已過時。
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {//指示用戶是鎖定仍是解鎖。
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() { //指示用戶的憑證(密碼)是否已過時。
        return true;
    }

    @Override
    public boolean isEnabled() {    //指示用戶是啓用仍是禁用。
        return true;
    }

    public Log getLogger() {
        return logger;
    }

    public void setLogger(Log logger) {
        this.logger = logger;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public List<MyGrantedAuthority> getList() {
        return list;
    }

    public void setList(List<MyGrantedAuthority> list) {
        this.list = list;
    }
}

        自定義登陸數據庫

                這個把資源賦值給用戶實際是要在登陸成功以後執行的代碼,Demo 就將就的看看吧,比較是學習用的,只要掌握流程便好。apache

public class MyUserDetailsService implements UserDetailsService {

    public Log logger = LogFactory.getLog(MyUserDetailsService.class);

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("\n\n ------> > Login Find By UserName :" + username +"\n\n");
        UserInfo user = new UserInfo("admin", "admin123");
        if(!"admin".equals(username)){
            throw new UsernameNotFoundException("用戶不存在");
        }
        user.setList(new ArrayList<MyGrantedAuthority>(Arrays.asList(new MyGrantedAuthority[]{
                new MyGrantedAuthority(1,"F1"),
                new MyGrantedAuthority(1,"F2"),
                new MyGrantedAuthority(1,"F3"),
        })));
        return user;
    }
}

 

第二部分->用戶權限api

        自定義權限須要實現GrantedAuthority的接口cookie

@Data
class MyGrantedAuthority implements GrantedAuthority{
    private Integer id;
    private String code;

    public MyGrantedAuthority() {
    }

    public MyGrantedAuthority(Integer id, String code) {
        this.id = id;
        this.code = code;
    }

    @Override
    public String getAuthority() {
        return code;
    }
}

第三部分->驗證碼確認

        驗證碼認證明現

/**
 * 驗證碼
 *      驗證碼認證 -> 可改用 Redis 實現。
 */
protected class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public LoginAuthenticationFilter() {
        super();
        setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(login_failure));
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        if ("POST".equalsIgnoreCase(req.getMethod()) && login.equals(req.getServletPath())) {
            String vcode = req.getParameter("vcode");
            if (vcode != null && !vcode.equalsIgnoreCase("X1234")) {
                unsuccessfulAuthentication(req, res, new InsufficientAuthenticationException("VCode Error"));
                return;
            }
        }
        chain.doFilter(request, response);
    }

}

        驗證碼實現加入到SecurityContext 上下文中

http.addFilterBefore(new LoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

第四部分->核心配置理解

        Session 管理使用了最Low 的解決方案 ,在登陸成功以後把 SecurityContext  存儲到Redis中,而後在全部請求致以前進行攔截經過Sid 判斷是否登陸,沒有登陸而且有Sid 則去Redis 中尋找SecurityContext  而後加入到系統當中。

        SecurityContext   不能用FastJson 作序列化,序列化以後反序列化會出現數據不對的狀況。這裏所使用的是Io流加Base64的方式序列化成字符串而後存入Redis 而後反序列化出來的。

        Spring Mvc 加入 Spring Security 的兩種方式

                1》Java Config  繼承 AbstractSecurityWebApplicationInitializer

                2》Web Xml Config 

                 說明:兩種方式都是爲了註冊一個 springSecurityFilterChain 的攔截器,只能使用一種配置,不然會報錯,出現兩個springSecurityFilterChain 攔截器的。

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

        Spring Security 核心配置 過濾器  

package com.pw.test.controller.config;

import cn.hutool.core.codec.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.InsufficientAuthenticationException;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/***
 * <pre>
 *
 *     Web.xml 配置
 *
 *          <filter>
 *               <filter-name>springSecurityFilterChain</filter-name>
 *               <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
 *           </filter>
 *           <filter-mapping>
 *               <filter-name>springSecurityFilterChain</filter-name>
 *               <url-pattern>/*</url-pattern>
 *           </filter-mapping>
 *
 *   Spring Security 實現功能
 *          SecurityContext 實現 Redis 存儲 , 解決Security 集羣Session 不統一的問題
 *                  Redis 存儲解決方案
 *                     SavedRequestAwareAuthenticationSuccessHandler    -> SecurityContext 先序列化 轉 Base64 字符串 存儲到 Redis 中
 *                     HttpSessionSecurityContextRepository             -> 攔截全部的請求 , 若是有SID 則用SID 到Redis 中取數據 , 而後反序列化 存入 HTTPSession 中
 *                     SecurityContextLogoutHandler                     -> 退出操做 刪除 Redis 中的數據
 *
 *          實現驗證碼功能。
 *          實現登陸提示中文化。
 *
 *
 *
 * </pre>
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    public Log logger = LogFactory.getLog(MyUserDetailsService.class);

    public String login = "/login";
    public String login_out = "/logout";
    public String login_out_url = "/";
    public String login_success = "/success";
    public String login_failure = "/login?error=true";

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 全部請求以前
     *      反寫到HttpSession 中
     */
    public class MyHttpSessionSecurityContextRepository extends HttpSessionSecurityContextRepository {
        public MyHttpSessionSecurityContextRepository() {
            super();
        }

        @Override
        public SecurityContext loadContext(HttpRequestResponseHolder holder) {
            SecurityContext context = super.loadContext(holder);
            //TODO 反寫 SecurityContext
            if(null == context || null == context.getAuthentication() || null == context.getAuthentication().getPrincipal()) {
                //已登陸用戶
                String sid = getSid(holder.getRequest().getCookies());
                if(null != sid){
                    String contextString = redisTemplate.opsForValue().get(sid);
                    if(null != contextString){
                        context = (SecurityContext)fromSerializableString(contextString);
                    }
                }
            }
            return context;
        }
    }

    /**
     * 退出操做
     *      刪除 Redis 中的數據
     */
    public class MySecurityContextLogoutHandler extends SecurityContextLogoutHandler {
        //TODO 登出 刪除 Redis 的Sid
        @Override
        public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
            String sid = getSid(request.getCookies());
            redisTemplate.opsForValue().getOperations().delete(sid);
            super.logout(request, response, authentication);
        }
    }

    /**
     * 登陸成功
     *     登錄成功把SecurityContext 存儲到 Redis 中
     */
    protected class SuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

        //TODO SecurityContext 寫入到 Redis 中
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            SecurityContext context = SecurityContextHolder.getContext();
            if(null != context && null != context.getAuthentication() && null != context.getAuthentication().getPrincipal()) {
                //已登陸用戶
                String sid = getSid(request.getCookies());
                if(null == sid){
                    sid = UUID.randomUUID().toString();
                    response.addCookie(new Cookie("sid",sid));
                }
                redisTemplate.opsForValue().set(sid, toSerializableString(context), 180, TimeUnit.SECONDS);
            }
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }


    /**
     * 驗證碼
     *      驗證碼認證 -> 可改用 Redis 實現。
     */
    protected class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

        public LoginAuthenticationFilter() {
            super();
            setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(login_failure));
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse res = (HttpServletResponse) response;
            if ("POST".equalsIgnoreCase(req.getMethod()) && login.equals(req.getServletPath())) {
                String vcode = req.getParameter("vcode");
                if (vcode != null && !vcode.equalsIgnoreCase("X1234")) {
                    unsuccessfulAuthentication(req, res, new InsufficientAuthenticationException("VCode Error"));
                    return;
                }
            }
            chain.doFilter(request, response);
        }

    }


    /***
     * 登陸錯誤提示 中文
     * org.springframework.security.messages
     * @return
     */
    @Bean
    public MessageSource messageSource() {
        Locale.setDefault(Locale.SIMPLIFIED_CHINESE);
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.addBasenames("classpath:org/springframework/security/messages");
        return messageSource;
    }



    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(new MyUserDetailsService()).passwordEncoder(new PasswordEncoder() {
            public String encode(CharSequence rawPassword) {
                return null;
            }

            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                logger.info("\n\n------> > matches CharSequence :" + rawPassword.toString() + "\t\t encodedPassword:" + encodedPassword + "\n\n");
                return rawPassword.toString().equals(encodedPassword);
            }
        });
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        logger.info("\n\n ---> ---> WebSecurityConfig extends WebSecurityConfigurerAdapter configure(HttpSecurity http) \n\n");

        http.securityContext().securityContextRepository(new MyHttpSessionSecurityContextRepository());

        http.sessionManagement().enableSessionUrlRewriting(true);
        http.csrf().disable();
        http
                .authorizeRequests()                                                            //方法有多個子節點,每一個macher按照他們的聲明順序執行。
                .antMatchers("/css/**", "/signup", "/about").permitAll()           //咱們指定任何用戶均可以訪問多個URL的模式。 任何用戶均可以訪問以"/resources/","/signup", 或者 "/about"開頭的URL。
                .antMatchers("/admin/**").hasRole("ADMIN")                         //以 "/admin/" 開頭的URL只能由擁有 "ROLE_ADMIN"角色的用戶訪問。請注意咱們使用 hasRole 方法,沒有使用 "ROLE_" 前綴
                .anyRequest().authenticated()                                                   //是對http全部的請求必須經過受權認證才能夠訪問。
                .and()
                .formLogin()                                                                    //指定登陸頁的路徑
                .loginPage(login)                                                            //自定義登陸頁頁面
                .usernameParameter("userLoginName")                                             //登陸名參數必須被命名爲userLoginName
                .passwordParameter("userLoginPassword")                                         //密碼參數必須被命名爲userLoginPassword

                .defaultSuccessUrl(login_success)
                .successHandler(new SuccessHandler())
//
//                .defaultSuccessUrl("/index")                                                    //登陸成功後處理頁面
//                .successHandler(new AuthenticationSuccessHandler() {                          //登陸成功後處理
//                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//
//                    }
//                })


                .failureUrl(login_failure)                                              //登陸失敗後處理頁面
//                .failureHandler(new MyAuthenticationFailureHandler())
//                .failureHandler(new AuthenticationFailureHandler() {                          //登陸失敗後處理
//                    @Override
//                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//
//                    }
//                })
                .permitAll()                                                                    //咱們必須容許全部用戶訪問咱們的登陸頁(例如爲驗證的用戶),這個formLogin().permitAll()方法容許基於表單登陸的全部的URL的全部用戶的訪問。

                .and()
                .logout()                                                                       //提供註銷支持,使用WebSecurityConfigurerAdapter會自動被應用
                .logoutUrl(login_out)                                                           //設置觸發註銷操做的URL (默認是/logout). 若是CSRF內啓用(默認是啓用的)的話這個請求的方式被限定爲POST。 請查閱相關信息 JavaDoc相關信息.
                .logoutSuccessUrl(login_out_url)                                                          //註銷以後跳轉的URL。默認是/login?logout。具體查看 the JavaDoc文檔.
//                .logoutSuccessHandler(logoutSuccessHandler)                                     //讓你設置定製的 LogoutSuccessHandler。若是指定了這個選項那麼logoutSuccessUrl()的設置會被忽略。請查閱 JavaDoc文檔.
//                .invalidateHttpSession(true)                                                    //指定是否在註銷時讓HttpSession無效。 默認設置爲 true。 在內部配置SecurityContextLogoutHandler選項。 請查閱 JavaDoc文檔.
                .addLogoutHandler(new MySecurityContextLogoutHandler())                                                //添加一個LogoutHandler.默認SecurityContextLogoutHandler會被添加爲最後一個LogoutHandler。
//                .deleteCookies(cookieNamesToClear)                                              //容許指定在註銷成功時將移除的cookie。這是一個現實的添加一個CookieClearingLogoutHandler的快捷方式。
                .permitAll()
        ;

        http.addFilterBefore(new LoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);


    }




    public static String getSid(Cookie[] cookies){
        if(null != cookies &&cookies.length > 0){
            for (Cookie cookie : cookies) {
                if("sid".equals(cookie.getName())){
                    return cookie.getValue();
                }
            }
        }
        return null;
    }




    /**
     * Read the object from Base64 string.
     */
    private static Object fromSerializableString(String s)  {
        try {
            byte[] data = Base64.decode(s);
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
            Object o = ois.readObject();
            ois.close();
            return o;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Write the object to a Base64 string.
     */
    private static String toSerializableString(Object o) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(o);
            oos.close();
            return Base64.encode(baos.toByteArray());
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}
相關文章
相關標籤/搜索