栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java > SpringBoot

Spring Sceurity的开发1

SpringBoot 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Spring Sceurity的开发1

在开发实际应用项目当中,肯定存在用户登录和授权的过程,之前我们使用自己开发的权限框架或者 Shiro 来做这块内容的扩展和延伸,今天使用 Spring 框架自身的权限框架来集成下,也就是 Spring Security。

Spring Security 核心功能包括以下三个部分:
1)认证,解决你是谁的问题,也即用户登录;
2)授权,解决你可以干什么的问题,并不是你登录就可以为所欲为;
3)攻击防护,解决防止别人伪造身份问题,

1、基于表单的认证1.1第一印象

导入 springsecurity 的依赖包之后,我们的项目启动会自动开启基本安全认证,认证的用户名是 user,密码可以在控制台找到,形如


image


打开一个 URL,就可以看到需要登录验证


image


输入账号密码就可以正常使用。
当然这种情况是无法满足实际应用的,我们需要自己的用户名和密码来进行登录认证。

记下来首先配置一些 开启使用Springsecurity 的基本配置,在配置包里面新建一个类WebSecurityConfig,继承WebSecurityConfigurerAdapter,然后复写configure(HttpSecurity http)方法,具体代码如下

@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter{    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()//表单登录,身份认证
            .and()
            .authorizeRequests()//对请求授权
            .anyRequest()//任何请求
            .authenticated();//需要身份认证
    }
}

然后重启,进行访问,此时将会出现一个默认登录框,如下


image


账号密码和之前相同,可以测试,注意 URL 的跳转。

其实这个和之前没什么区别,也的确没什么区别,主要是原来默认的是http.httpBasic()改为了http.formLogin().

1.2基本原理

其实 Springsecurity 就是一组过滤器,形成过滤器练,所有的访问请求都会经过这些过滤器,这些过滤器在系统启动的时候自行配置到链中,无需开发者关心。
过滤器链上有很多过滤器,其中比较主要的几个
1、UserPasswordAuthenticationFilter、baseicAuthenticationFilter 是认证用户身份的,每个过滤器就是一种验证方式。和上面对应的就是
http.httpBasic()---->baseicAuthenticationFilter
http.formLogin()---->UserPasswordAuthenticationFilter
这两个过滤器都是检查请求里面是否包含过滤器需要的信息。比如先经过UserPasswordAuthenticationFilter这个过滤器,那么就需要先查看是否是一个登陆请求,是否包含用户名和密码,如果没有这些信息,那么就到baseicAuthenticationFilter过滤器,会检查请求头里面是否包含需要的信息。
2、一旦通过认证,会有相应的标记进行记录,然后继续想后面的过滤器传递。最后到达链的终端是FilterSecurityInterceptor,他是整个过滤器链最终守门人,他决定该请求能否顺利的访问 Controller 里面的服务。也就是说前面的链不管结论如何都会走到最后整个过滤器,有他来决定是往后继续执行业务,还是抛出某个异常。
3、一旦有异常出现,会在倒数第二层的过滤器来处理这些异常,这个过滤器是ExceptionTranslationFilter。他会根据具体的异常会导向不同的页面。


image


上图中,除了第一类(绿色)的我们可以控制,第二第三(橙色和蓝色)是不可控制的,他们一定在过滤器链的末端。

深入源码
分别在绿色的 UserPasswordAuthenticationFilter,蓝色的异常处理过滤器,橙色的过滤器,以及自己的 Rest 服务  上打上断点。
首先在 Controller 的方法上打个断点

image


其次在FilterSecurityInterceptor的124行地方打断点
[图片上传失败...(image-c0c504-1527080640040)]
再次在ExceptionTranslationFilter 的123行的地方打上断点

image


最后在UsernamePasswordAuthenticationFilter的获取用户名和面膜的地方打上断点

image


然后我们开始运行一个请求,比如http://localhost:8888/users?username=123。


首先断点停在FilterSecurityInterceptor这个类,运维前面的过滤器对这个 URL 都不care,由于我们配置了所有请求都需要身份验证,那么这关肯定过不去的。在执行beforeInvocation的时候跑出一个异常,这个异常跑出来之后,被ExceptionTranslationFilter过滤器捕获到了。然后对异常的处理,处理结果是一个重定向到一个登陆页面。
接下来进行登录,这次停在UsernamePasswordAuthenticationFilter这个过滤器上,说明登录请求被诸葛过滤器抓住了,并且开始进行验证登录结果。
再继续就又到橙色的FilterSecurityInterceptor类上,其实这之间由个跳转的,登录 URL 处理完毕,正常登录后又回到http://localhost:8888/users?username=123这个请求,其实是这个请求走到了橙色过滤器上。一旦InterceptorStatusToken token = super.beforeInvocation(fi);执行完毕,就到实际业务代码里面了。

1.2自定义用户认证逻辑1.2.1用户信息获取

这部分功能被封装在UserDetailsService这个接口里面。这个接口只有一个方法
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
接受用户名,返回UserDetails对象。创建一个类,实现UserDetailsService,并且实现方法,代码实现如下,为了简便处理,忽略了数据层

@Componentpublic class CustomerUserDetailService implements UserDetailsService{    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {        //根据用户名去数据库查询用户信息
        //可以注入 jdbc,mybatis 等 DAO 
        //这里方便演示,直接在代码里面做了
        
        //User 对象已经实现了UserDetails
        //AuthorityUtils.commaSeparatedStringToAuthorityList 方法是以逗号分割产生一个授权集合
        User user=new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));        return user;
    }
}

这下可以使用自己的用户登录逻辑了。

1.2.2校验逻辑

主要是密码是否匹配,比如取出123456密码之后交给 Springsecurity 就可以了。
其次其他的一些校验,密码过期,用户冻结等。
主要看 UserDetails这个接口,里面包含了所有信息

image


后面4个布尔返回方法可以执行自己的校验逻辑
isAccountNonExpired----账号是否过期
isAccountNonLocked----账号是否冻结,可恢复
isCredentialsNonExpired----密码是否过期
isEnabled----账号是否删除,不可恢复
我们在构造 User 的时候使用有7参数的构造,改写


User user=new User(username, "123456", 
                true,//账号可用
                true,//账号不过期
                true,//密码不过期
                true,//账号没有锁定
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));        return user;
1.2.3加密解密

实际应用密码取出应该是一个加密的密码,而不是明文。Springsecurity 的密码加密在接口PasswordEncoder中处理。该接口有2个方法,分别是
String encode(CharSequence rawPassword);负责加密,用户注册的时候对明文加密,存到 DB。
boolean matches(CharSequence rawPassword, String encodedPassword);负责匹配
为了使用加密功能,这里使用一个美人的实现BCryptPasswordEncoder,把这个 bean 添加到配置中。

image


为了演示,这里直接使用

image


1.3个性化登录

1、自定义用户登录页
之前 SpringSecurity 自带的登录页当然不能正常使用,我们需要自行定制一个,首先在配置里面增加一行登录页的名称

image


然后开始构建这个页面

image


需要注意的是:这个 login.html 需要排除请求认证之外。

image


这样我们就能使用自己的登录页面了。

image


登录的请求地址是/userlogin,form是2.2 校验短信验证码和登录

和用户密码登录类似构造自己的过滤器,但是短信验证码校验是放在过滤器之前,方便通用。

image


接下来开始逐个实现
SMSCodeAuthenticationToken模仿UsernamePasswordAuthenticationToken代码,稍作修改


//封装登录信息public class SMSCodeAuthenticationToken extends AbstractAuthenticationToken {    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;    // ~ Instance fields
    // ================================================================================================

    private final Object principal;//未验证之前是手机号,验证之后是用户信息
    //private Object credentials;//密码,由于在这之前已经验证过了,所以不需要这个属性

    // ~ Constructors
    // ===================================================================================================

    
    public SMSCodeAuthenticationToken(String mobile) {        super(null);        this.principal = mobile;
        setAuthenticated(false);
    }    
    public SMSCodeAuthenticationToken(Object principal, Object credentials,
            Collection authorities) {        super(authorities);        this.principal = principal;        super.setAuthenticated(true); // must use super, as we override
    }    // ~ Methods
    // ========================================================================================================

    

    public Object getPrincipal() {        return this.principal;
    }    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {        if (isAuthenticated) {            throw new IllegalArgumentException(                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }        super.setAuthenticated(false);
    }    @Override
    public void eraseCredentials() {        super.eraseCredentials();
    }    @Override
    public Object getCredentials() {        return null;
    }
}

SMSCodeAuthenticationFilter同样模仿UsernamePasswordAuthenticationFilter

public class SMSCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {    public static final String MOBILE = "mobile";    private String mobileParameter = MOBILE;    private boolean postOnly = true;    public SMSCodeAuthenticationFilter() {        super(new AntPathRequestMatcher("/mobilelogin", "POST"));
    }    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {        if (postOnly && !request.getMethod().equals("POST")) {            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        String mobile = obtainMobile(request);        if (mobile == null) {
            mobile = "";
        }

        mobile = mobile.trim();

        SMSCodeAuthenticationToken authRequest = new SMSCodeAuthenticationToken(mobile);        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);        return this.getAuthenticationManager().authenticate(authRequest);
    }    protected String obtainMobile(HttpServletRequest request) {        return request.getParameter(mobileParameter);
    }    protected void setDetails(HttpServletRequest request, SMSCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }    public void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "mobileParameter parameter must not be empty or null");        this.mobileParameter = mobileParameter;
    }    public void setPostonly(boolean postOnly) {        this.postOnly = postOnly;
    }

}

SMSCodeAuthenticationProvider类的代码如下

public class SMSCodeAuthenticationProvider implements AuthenticationProvider{    private UserDetailsService userDetailsService;    
    //使用 UserDetailService 获取用户信息重新组装 Authentication
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SMSCodeAuthenticationToken token=(SMSCodeAuthenticationToken)authentication;
        UserDetails user=userDetailsService.loadUserByUsername((String)token.getPrincipal());        if(user==null){            throw new InternalAuthenticationServiceException("无法获取用户信息");
        }
        SMSCodeAuthenticationToken result=new SMSCodeAuthenticationToken(user,user.getAuthorities());
        result.setDetails(token.getDetails());//需要把之前的 Detail 设置到新的 Token 里面
        return result;
    }    //检查参数是不是我们定义的 SMSCodeAuthenticationToken
    @Override
    public boolean supports(Class authentication) {        return SMSCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }    public UserDetailsService getUserDetailsService() {        return userDetailsService;
    }    public void setUserDetailsService(UserDetailsService userDetailsService) {        this.userDetailsService = userDetailsService;
    }

}

最后SMSCodeFilter雷同之前的图形验证码的过滤器,需要修改部分代码即可

//OncePerRequestFilter 保证过滤器每次只被调用一次public class SMSCodeFilter extends OncePerRequestFilter{    private AuthenticationFailureHandler authenticationFailureHandler;    
    private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {        //判断拦截的请求 URL,只是登录的 URL
        //并且是 POST 请求
        if(StringUtils.equals("/mobilelogin", request.getRequestURI())
                && StringUtils.equalsIgnoreCase("post", request.getMethod())){            try {
                validate(new ServletWebRequest(request));
            } catch (ImageCodeException e) {//自定义异常进行捕获
                //一旦出现异常,使用authenticationFailureHandler来处理
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);                return;
            }
        }
        filterChain.doFilter(request, response);
        
        
    }    private void validate(ServletWebRequest request) throws ServletRequestBindingException {        //分别拿到 session 和请求里面的验证码信息
        SMSCode codeInSession=(SMSCode)sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");        
        if(StringUtils.isEmpty(codeInRequest)){            throw new ImageCodeException("验证码不能为空");
        }        
        if(codeInSession==null){            throw new ImageCodeException("验证码不存在");
        }        
        if(codeInSession.isExpire()){
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY_SMS);            throw new ImageCodeException("验证码过期");
        }        if(!StringUtils.equals(codeInSession.getCode(), codeInRequest)){            throw new ImageCodeException("验证码不匹配");
        }        //清除 session 里面的 ImageCode
        sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
        
    }    public AuthenticationFailureHandler getAuthenticationFailureHandler() {        return authenticationFailureHandler;
    }    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {        this.authenticationFailureHandler = authenticationFailureHandler;
    }    public SessionStrategy getSessionStrategy() {        return sessionStrategy;
    }    public void setSessionStrategy(SessionStrategy sessionStrategy) {        this.sessionStrategy = sessionStrategy;
    }
    
    

}

最终配置以上代码,使其可以正常工作
分连部分,首先配置前三个,配到核心包里面

@Componentpublic class SMSCodeAuthenticationConfig extends SecurityConfigurerAdapter{    @Autowired private AuthenticationFailureHandler myAuthenticationFailureHandler;    @Autowired private AuthenticationSuccessHandler myAuthenticationSuccessHandler;    @Autowired private UserDetailsService userDetailsService;    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        SMSCodeAuthenticationFilter sMSCodeAuthenticationFilter=new SMSCodeAuthenticationFilter();
        sMSCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        sMSCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
        sMSCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
        
        SMSCodeAuthenticationProvider sMSCodeAuthenticationProvider=new SMSCodeAuthenticationProvider();
        sMSCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
        
        http.authenticationProvider(sMSCodeAuthenticationProvider)
        .addFilterAfter(sMSCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

再把SMSCodeFilter类似ValidateCodeFilter在 SS 配置中进行

image


最后把SMSCodeAuthenticationConfig也添加到 SS 配智中。
先引入,最后 apply(sMSCodeAuthenticationConfig);
最后贴出完整代码


@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter{    @Autowired
    private SecurityProperties securityProperties;    
    @Bean
    public PasswordEncoder passwordEncoder(){        return new BCryptPasswordEncoder();
    }    
    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;    
    @Autowired
    private DataSource dataSource;    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl=new JdbcTokenRepositoryImpl();
        jdbcTokenRepositoryImpl.setDataSource(dataSource);        //jdbcTokenRepositoryImpl.setCreateTableonStartup(true);//执行一次,创建表,也可以自行创建表
        return jdbcTokenRepositoryImpl;
    }    @Autowired
    private UserDetailsService userDetailsService;    
    @Autowired
    private SMSCodeAuthenticationConfig sMSCodeAuthenticationConfig;    
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("Config");
        ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
        
        SMSCodeFilter smsCodeFilter=new SMSCodeFilter();
        smsCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
        
        
        http
            .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
            .formLogin()//表单登录,身份认证
            .loginPage("/authlogin")//设置登录页
            .loginProcessingUrl("/userlogin")//设置表单提交请求的 URL
            
            .successHandler(myAuthenticationSuccessHandler)
            .failureHandler(myAuthenticationFailureHandler)
            
            .and()
            .rememberMe()
            .tokenRepository(persistentTokenRepository())
            .tokenValiditySeconds(securityProperties.getWeb().getRememberMeSecond())
            .userDetailsService(userDetailsService)
            
            .and()
            .authorizeRequests()//对请求授权
            .antMatchers("/authlogin","/mobilelogin",                    "/login.html",//默认的登录页
                    "/image/code",                    "/sms/code",
                    securityProperties.getWeb().getLoginPage())
            .permitAll()//表示对这个 url 永远的通过
            .anyRequest()//任何请求
            .authenticated()//需要身份认证
            .and()
            .csrf().disable()//把 csdf 跨站防护关闭
            .apply(sMSCodeAuthenticationConfig);
    }
}



作者:breezedancer
链接:https://www.jianshu.com/p/96e67ceb8fd8

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/235679.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号