spring-security-oauth2(二) 自定义个性化登录

自定义认证逻辑

1.认证逻辑接口

spring-security用户登录逻辑验证接口org.springframework.security.core.userdetails.UserDetailsService只有一个方法

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

UserDetail信息如下:我们自定义的用户信息要实现这个接口,

public interface UserDetails extends Serializable {

    //权限相关
    Collection<? extends GrantedAuthority> getAuthorities();
    //获取密码
    String getPassword();
    //获取用户名
    String getUsername();
    //账户是否验证过期
    boolean isAccountNonExpired();
    //账户是否锁定
    boolean isAccountNonLocked();
     //账户验证是否过期
    boolean isCredentialsNonExpired();
    //账户是否有效
    boolean isEnabled();
}

org.springframework.security.core.userdetails.User这个是它的一个实现

this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));//线程安全的权限添加 同时有内部类自定义排序

2.处理密码加密解密

配置了这个Bean以后,从前端传递过来的密码就会被加密,所以从数据库查询到的密码必须是经过加密的,而这个过程都是在用户注册的时候进行加密的。这就合理解释了为什么对上面的代码进行加密了。

org.springframework.security.crypto.password.PasswordEncoder

public interface PasswordEncoder {
    //加密
    String encode(CharSequence var1);
    //验证是否匹配
    boolean matches(CharSequence var1, String var2);
}

 在浏览器权限配置类BrowserSecurityConfig中注入这个bean

/**
 * 浏览器security配置类
 *
 * @author CaiRui
 * @date 2018-12-4 8:41
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 密码加密解密
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
                //spring5后默认就是表单登录方式
                // httpBasic().
                        formLogin().
                and().
                authorizeRequests().
                anyRequest().
                authenticated();
    }

}

3.自定义接口实现

package com.rui.tiger.auth.browser.user;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * 自定义用户登录实现
 *
 * @author CaiRui
 * @date 2018-12-5 8:19
 */
@Component
@Slf4j
public class MyUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //TODO 后续做成数据库实现(MyBaites-plus实现)先实现流程
        //1.根据用户名去数据库去查询用户信息获取加密后的密码 这里模拟一个加密的数据库密码
        String encryptedPassWord = passwordEncoder.encode("123456");
        log.info("模拟加密后的数据库密码:{}",encryptedPassWord);
        //2.这里可以去验证账户的其它相关信息 默认都通过
        
        //3.返回认证过的用户信息  授予一个admin的权限
        return new User(username,
                encryptedPassWord,
                true,
                true,
                true,
                true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

实现完了我们启动项目来验证下配置的MyUserDetailServiceImpl是否成功了,可以看到默认的随机密码在控制台已经没有了。浏览器随便访问一个地址,会调到默认的登录表单界面

密码我们先随便输入一个 比如66666

可以看到登录失败,我们再输入我们固定的密码123456

 可以看到我们登录成功,所以出现这个界面是因为http://localhost:8070/user这个我没有实现,验证成功后重定向到之前的地址   同时我们可以看到控制台也会打印如下信息 证明我们的自定义认证成功。ok下面我们开始实现自己的个性化登录需求开发

4.个性化登录实现

在实际开发中通常我们都不会使用spring-security默认的登录界面,我们可以通过配置实现自己的个性化登录,下面是具体实现。

1)自定义登录页面

首先修改我们的浏览器配置类BrowserSecurityConfig,同时要在资源文件下添加我们的自定义登录界面/tiger-login.html

package com.rui.tiger.auth.browser.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 浏览器security配置类
 *
 * @author CaiRui
 * @date 2018-12-4 8:41
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 密码加密解密
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                .loginPage("/tiger-login.html")//自定义标准登录界面
                .and()
                .authorizeRequests()
                .antMatchers("/tiger-login.html")//此路径放行 否则会陷入死循环
                .permitAll()
                .anyRequest()
                .authenticated();
    }

}

tiger-login.html文件如下,注意放置的路径

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>标准登录页面</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="/authentication/form" method="post">
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>
</body>
</html>

ok 我们来启动项目输入http://localhost:8070/user  看看效果,可以看见已经成功跳到我们的自定义界面了

 我们再次输入用户名user和密码123456试试看

可以看见又重定向到我们的tiger-login.html,这是怎么回事呢?

原来是是我们的 tiger-login.html定义的表单请求<form action="/authentication/form" method="post">和spring-security默认的表单登录请求不一致,参见UsernamePasswordAuthenticationFilter源码如下:

public UsernamePasswordAuthenticationFilter() {
   super(new AntPathRequestMatcher("/login", "POST"));
}

我们只要BrowserSecurityConfig添加自定义表单的请求路径就可以loginProcessingUrl("/authentication/form"),同时进行权限放行,并关闭跨域访问,相关配置如下

package com.rui.tiger.auth.browser.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 浏览器security配置类
 *
 * @author CaiRui
 * @date 2018-12-4 8:41
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 密码加密解密
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                .loginPage("/tiger-login.html")//自定义标准登录界面
                .loginProcessingUrl("/authentication/form")//自定义表单请求路径
                .and()
                .authorizeRequests()
                .antMatchers("/tiger-login.html","/authentication/form")//此路径放行 否则会陷入死循环
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable()//跨域关闭
        ;
    }

}

再次访问 localhost:8070/user/hello  可以看到api可以成功访问了

这里虽然配置了自定义的路径,但都是统一跳转到了静态界面,在现在流行的前后台分离的项目中,返回给前台的通常都是一个json串,那么要怎么实现 根据请求来分发是返回html内容?还是返回json内容呢? 

处理不同类型的请求

由于我们程序中有很多信息来自配置文件,下面我们用类来统一管理请看下面实现,先看下他们的关系

SecurityPropertie 权限配置父类

          BrowserProperties 浏览器相关配置

          AppProperties   移动端相关配置

          SocialProperties 社交相关配置

         CaptchaProperties 验证码相关配置

         。。。。。。。。。。  

由于这些配置类是browser和app项目公用的,所以写在核心模块core里 

package com.rui.tiger.auth.core.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * 权限配置文件父类(注意这里不用lombok 会读取不到)
 * 这里会有很多权限配置子模块
 * @author CaiRui
 * @date 2018-12-6 8:41
 */

@ConfigurationProperties(value = "tiger.auth",ignoreInvalidFields = true)
public class SecurityProperties {

    /**
     * 浏览器配置类
     */
    private BrowserProperties browser = new BrowserProperties();

    public BrowserProperties getBrowser() {
        return browser;
    }

    public void setBrowser(BrowserProperties browser) {
        this.browser = browser;
    }
}

 BrowserProperties 浏览器配置如下:

package com.rui.tiger.auth.core.properties;

import com.rui.tiger.auth.core.model.enums.LoginTypeEnum;

/**
 * 浏览器配置
 *
 * @author CaiRui
 * @date 2018-12-6 8:42
 */
public class BrowserProperties {
    /**
     * 登录页面 不配置默认标准登录界面
     */
    private String loginPage = "/tiger-login.html";
    /**
     * 跳转类型 默认返回json数据
     */
    private LoginTypeEnum loginType = LoginTypeEnum.JSON;


    public String getLoginPage() {
        return loginPage;
    }

    public void setLoginPage(String loginPage) {
        this.loginPage = loginPage;
    }

    public LoginTypeEnum getLoginType() {
        return loginType;
    }

    public void setLoginType(LoginTypeEnum loginType) {
        this.loginType = loginType;
    }
}

还要一个配置类SecurityPropertiesCoreConfig来使上面的配置生效 

package com.rui.tiger.auth.core.config;

import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * SecurityProperties 配置类注入生效
 *
 * @author CaiRui
 * @date 2018-12-6 8:57
 */
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityPropertiesCoreConfig {
    
}

项目application.yml配置文件如下配置

spring:
  datasource:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://my.yunout.com:3306/tiger_study?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
    # 配置Druid连接池
    type: com.alibaba.druid.pool.DruidDataSource
  session:
    store-type: none

# Tomcat
server:
  port: 8070
  connection-timeout: 5000ms
#自定义权限配置
tiger:
  auth:
     browser:
      #loginPage: /demo-login.html # 这里可以配置成自己的非标准登录界面
      loginType: JSON

LoginTypeEnum是BrowserProperties中控制跳转行为的枚举类

package com.rui.tiger.auth.core.model.enums;

import lombok.Getter;

/**
 * 登录类型枚举类
 * @author CaiRui
 * @date 2018-12-6 12:45
 */
@Getter
public enum LoginTypeEnum {

    /**
     * json数据返回
     */
    JSON,
    /**
     * 重定向
     */
    REDIRECT;
}

ok 上面权限配置类都准备完成了,修改浏览器配置类,使其登录路径是我们自定义的控制器路径,里面控制是返回josn 还是html界面,

同时里面还要我们自定义的登录成功和失败处理器,这个我们稍后来说。

package com.rui.tiger.auth.browser.config;

import com.rui.tiger.auth.core.authentication.TigerAuthenticationFailureHandler;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationSuccessHandler;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 浏览器security配置类
 *
 * @author CaiRui
 * @date 2018-12-4 8:41
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;
    @Autowired
    private TigerAuthenticationFailureHandler tigerAuthenticationFailureHandler;
    @Autowired
    private TigerAuthenticationSuccessHandler tigerAuthenticationSuccessHandler;

    /**
     * 密码加密解密
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                .loginPage( "/authentication/require")//自定义登录请求
                .loginProcessingUrl("/authentication/form")//自定义表单登录地址
                .successHandler(tigerAuthenticationSuccessHandler)
                .failureHandler(tigerAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers(securityProperties.getBrowser().getLoginPage(),
                        "/authentication/require")//此路径放行 否则会陷入死循环
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable()//跨域关闭
        ;
    }

}

编写处理请求的处理器BrowserRequireController

package com.rui.tiger.auth.browser.controller;

import com.rui.tiger.auth.core.properties.SecurityProperties;
import com.rui.tiger.auth.core.support.SimpleResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 用户登录认证控制器
 *
 * @author CaiRui
 * @date 2018-12-5 12:44
 */
@RestController
@Slf4j
public class BrowserRequireController {

    //封装了引发跳转请求的工具类  https://blog.csdn.net/honghailiang888/article/details/53671108
    private RequestCache requestCache = new HttpSessionRequestCache();
    // spring的工具类:封装了所有跳转行为策略类
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    private static final String HTML_SUFFIX = ".html";



    /**
     * 当需要进行身份认证的时候跳转到此方法
     *
     * @param request  请求
     * @param response 响应
     * @return 将信息以JSON形式返回给前端
     */
    @RequestMapping("/authentication/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
        log.info("BrowserRequireController进来了 啦啦啦");
        // 从session缓存中获取引发跳转的请求
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (null != savedRequest) {
            String redirectUrl = savedRequest.getRedirectUrl();
            log.info("引发跳转的请求是:{}", redirectUrl);
            if (StringUtils.endsWithIgnoreCase(redirectUrl, HTML_SUFFIX)) {
                // 如果是HTML请求,那么就直接跳转到HTML,不再执行后面的代码
                redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
            }
        }
        return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页面");
    }


}

同时编写我们的登录成功TigerAuthenticationSuccessHandler和失败处理器TigerAuthenticationFailureHandler,这里可以加入我们的一些逻辑 比如登录成功记录日志,这里只是返回json还是重定向处理,通过配置 BrowserProperties中的loginType就可以实现,参看上面。

TigerAuthenticationSuccessHandler

package com.rui.tiger.auth.core.authentication;

import com.alibaba.fastjson.JSON;
import com.rui.tiger.auth.core.model.enums.LoginTypeEnum;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 认证成功处理器
 * {@link SavedRequestAwareAuthenticationSuccessHandler}是Spring Security默认的成功处理器
 * @author CaiRui
 * @date 2018-12-6 12:39
 */
@Component("tigerAuthenticationSuccessHandler")
@Slf4j
public class TigerAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
       log.info("登录成功");
       if(LoginTypeEnum.JSON.equals(securityProperties.getBrowser().getLoginType())){
           //返回json处理 默认也是json处理
           response.setContentType("application/json;charset=UTF-8");
           log.info("认证信息:"+JSON.toJSONString(authentication));
           response.getWriter().write(JSON.toJSONString(authentication));
       } else {
           // 如果用户定义的是跳转,那么就使用父类方法进行跳转
           super.onAuthenticationSuccess(request, response, authentication);
       }

    }
}

TigerAuthenticationFailureHandler

package com.rui.tiger.auth.core.authentication;

import com.alibaba.fastjson.JSON;
import com.rui.tiger.auth.core.model.enums.LoginTypeEnum;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 认证失败处理器
 * @author CaiRui
 * @date 2018-12-6 12:40
 */
@Component("tigerAuthenticationFailureHandler")
@Slf4j
public class TigerAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("登录失败");
        if (LoginTypeEnum.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(JSON.toJSONString(exception));
        } else {
            // 如果用户配置为跳转,则跳到Spring Boot默认的错误页面
            super.onAuthenticationFailure(request, response, exception);
        }

    }
}

ok 下面我们来测试下看我们的流程是否可以?

如果我们直接访问 localhost:8070/user/hello 

这是因为我们默认配置了json,输入我们的的登录表单地址localhost:8070/tiger-login.html,并输入正确的账户密码登录

可以看到已经返回认证成功的json字符串,失败处理器也会返回失败的信息这里就不测试了。

到现在整个登录基本流程算是跑通了,下一章我们来简单分析下spring-security的认证源码。

‘’

TigerAuthenticationFailureHandlerTigerAuthenticationFailureHandler

猜你喜欢

转载自blog.csdn.net/ahcr1026212/article/details/84813955