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;
    }
}
相关文章
相关标签/搜索