springsecurity讲解2

本文用示例的方式讲解,springsecurity,使用session方式,
用户名密码和手机验证码两种方式
非常简陋的登入页面


image.png

该示例的代码


image.png

CustomAuthenticationFailureHandler 失败处理器
/**
认证失败处理器
 **/
@Component
public class CustomAuthenticationFailureHandler  extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException{
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("认证失败");
    }
}

CustomAuthenticationSuccessHandler 成功处理器

/**
认证成功处理器
 **/
@Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException{
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("认证成功");
    }
}

CustomUserDetailsService 获取用户信息

/**
 *
 * 模拟从数据库获取用户信息,这里要注意的是,如果在配置的时候使用内存的方式,是不回使用该services
 * SpringSecurityConfiguration方法中规定了使用那种方式管理用户信息,本例使用的是内存的方式
 * 所以在用户名密码模式的时候,不回执行loadUserByUsername,手机登入的时候还是会走loadUserByUsername方法
 */
@Configuration
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
        //封装用户信息,用户名和密码和权限,注意这里要注意密码应该是加密的
        //省略从数据库获取详细信息
        return new User(username, "1234",
                AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
    }
}

SpringSecurityConfiguration security整体配置


@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    SecurityConfigurerAdapter mobileAuthenticationConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/login.html","/code/mobile").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .successHandler(new CustomAuthenticationSuccessHandler())
                .failureHandler(new CustomAuthenticationFailureHandler())
                .loginPage("/login")
        ;  //浏览器以form表单形式
        //将手机验证码配置放到http中,这样mobileAuthenticationConfig配置就会生效
        http.apply(mobileAuthenticationConfig);

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        // 用户信息存储在内存中
        auth.inMemoryAuthentication().withUser("user")
            .password(new BCryptPasswordEncoder().encode("1234")).authorities("ADMIN");
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/code/mobile");
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        // 官网建议的加密方式,相同的密码,每次加密都不一样,安全性更好一点
        return new BCryptPasswordEncoder();
    }
}


CacheValidateCode 手机验证码的内存存储

/**
 * 将手机验证码保存起来,后续验证中,实际项目中要放到redis等存储
 **/
public class CacheValidateCode {
    public static ConcurrentHashMap<String, String> cacheValidateCodeHashMap = new ConcurrentHashMap();

}

MobileAuthenticationConfig 手机验证码配置类,在SpringSecurityConfiguration中通过http.apply方式放到springsecurity中

/**
 * 用于组合其他关于手机登录的组件
 */
@Component
public class MobileAuthenticationConfig
        extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    @Autowired
    CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
    @Autowired
    UserDetailsService mobileUserDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception{

        MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
        // 获取容器中已经存在的AuthenticationManager对象,并传入 mobileAuthenticationFilter 里面
        mobileAuthenticationFilter.setAuthenticationManager(
                http.getSharedObject(AuthenticationManager.class));


        // 传入 失败与成功处理器
        mobileAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
        mobileAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);

        // 构建一个MobileAuthenticationProvider实例,接收 mobileUserDetailsService 通过手机号查询用户信息
        MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
        provider.setUserDetailsService(mobileUserDetailsService);

        // 将provider绑定到 HttpSecurity上,并将 手机号认证过滤器绑定到用户名密码认证过滤器之后
        http.authenticationProvider(provider)
            .addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    }
}

MobileAuthenticationFilter 手机验证filter,完全模仿UsernamePasswordAuthenticationFilter

/**
 * 用于校验用户手机号是否允许通过认证
 * 完全复制 UsernamePasswordAuthenticationFilter
 */
public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private String mobileParameter = "mobile";
    private String validateCodeParameter = "code";
    private boolean postOnly = true;


    public MobileAuthenticationFilter(){
        super(new AntPathRequestMatcher("/mobile/form", "POST"));
    }

    // ~ Methods
    // ========================================================================================================

    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);
        String validateCode = obtainValidateCode(request);

        if(mobile == null){
            mobile = "";
        }

        mobile = mobile.trim();

        MobileAuthenticationToken authRequest = new MobileAuthenticationToken(mobile, validateCode);

        // sessionID, hostname
        setDetails(request, authRequest);
        //认证手机码是否正确,通过provider的方式处理,使用哪个provider,是根据authRequest是哪个类型的token
        //这里放的是MobileAuthenticationToken
        return this.getAuthenticationManager().authenticate(authRequest);
    }


    /**
     * 从请求中获取手机号码
     */
    @Nullable
    protected String obtainMobile(HttpServletRequest request){
        return request.getParameter(mobileParameter);
    }

    /**
     * 从请求中获取验证码
     */
    @Nullable
    protected String obtainValidateCode(HttpServletRequest request){
        return request.getParameter(validateCodeParameter);
    }

    /**
     * 将 sessionID和hostname添加 到MobileAuthenticationToken
     */
    protected void setDetails(HttpServletRequest request,
                              MobileAuthenticationToken authRequest){
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }


    /**
     * 设置是否为post请求
     */
    public void setPostOnly(boolean postOnly){
        this.postOnly = postOnly;
    }

    public String getMobileParameter(){
        return mobileParameter;
    }

    public void setMobileParameter(String mobileParameter){
        this.mobileParameter = mobileParameter;
    }
}

MobileAuthenticationProvider 手机验证处理器

/**
 * 手机认证处理提供者,要注意supports方法和authenticate
 * supports判断是否使用当前provider
 * authenticate 验证手机验证码是否正确
 *
 */
public class MobileAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    public void setUserDetailsService(UserDetailsService userDetailsService){
        this.userDetailsService = userDetailsService;
    }

    /**
     * 认证处理:
     * 1. 通过手机号码 查询用户信息( UserDetailsService实现)
     * 2. 当查询到用户信息, 则认为认证通过,封装Authentication对象
     *
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException{
        MobileAuthenticationToken mobileAuthenticationToken =
                ( MobileAuthenticationToken ) authentication;
        // 获取手机号码
        String mobile = ( String ) mobileAuthenticationToken.getPrincipal();
        String validateCodeParameter = ( String ) mobileAuthenticationToken.getCredentials();
        // 通过 手机号码 查询用户信息( UserDetailsService实现)
        UserDetails userDetails =
                userDetailsService.loadUserByUsername(mobile);
        mobileAuthenticationToken.setDetails(userDetails);
        // 未查询到用户信息
        if(userDetails == null){
            throw new AuthenticationServiceException("该手机号未注册");
        }
        // 1. 判断 请求是否为手机登录,且post请求
        try{
            // 校验验证码合法性
            validate(mobile, validateCodeParameter);
        }catch(AuthenticationException e){
           throw new AuthenticationServiceException(e.getMessage());
        }
        //最终返回认证信息,这里要注意的是,返回的token中的authenticated字段要赋值为true
        return createSuccessAuthentication(mobileAuthenticationToken);
    }

    /**
     * 通过这个方法,来选择对应的Provider, 即选择MobileAuthenticationProivder
     *
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication){
        return MobileAuthenticationToken.class.isAssignableFrom(authentication);
    }


    private void validate(String mobile, String inpuCode){
        // 判断是否正确
        if(StringUtils.isEmpty(inpuCode)){
            throw new AuthenticationServiceException("验证码不能为空");
        }
        String cacheValidateCode = CacheValidateCode.cacheValidateCodeHashMap.get(mobile);
        if(!inpuCode.equalsIgnoreCase(cacheValidateCode)){
            throw new AuthenticationServiceException("验证码输入错误");
        }
    }

    protected Authentication createSuccessAuthentication(
            Authentication authentication){
        // Ensure we return the original credentials the user supplied,
        // so subsequent attempts are successful even with encoded passwords.
        // Also ensure we return the original getDetails(), so that future
        // authentication events after cache expiry contain the details
        MobileAuthenticationToken result = new MobileAuthenticationToken(
                authentication.getPrincipal(), authentication.getCredentials(),
                AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
        result.setDetails(authentication.getDetails());
        return result;
    }
}

MobileAuthenticationToken 手机验证码的token

/**
 * 创建自己的token,参考UsernamePasswordAuthenticationToken
 */
public class MobileAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // ~ Instance fields
    // ================================================================================================

    private final Object principal;
    private Object credentials;

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

    /**
     * This constructor can be safely used by any code that wishes to create a
     * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
     * will return <code>false</code>.
     */
    public MobileAuthenticationToken(Object principal, Object credentials){
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    /**
     * This constructor should only be used by <code>AuthenticationManager</code> or
     * <code>AuthenticationProvider</code> implementations that are satisfied with
     * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
     * authentication token.
     *
     * @param principal
     * @param credentials
     * @param authorities
     */
    public MobileAuthenticationToken(Object principal, Object credentials,
                                     Collection<? extends GrantedAuthority> authorities){
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }

    // ~ Methods
    // ========================================================================================================

    public Object getCredentials(){
        return this.credentials;
    }

    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();
        credentials = null;
    }
}

controller

@Controller
public class Congtroller {
    @RequestMapping("/code/mobile")
    @ResponseBody
    public String mobileCode(HttpServletRequest request){
        // 1. 生成一个手机验证码
        String code = RandomStringUtils.randomNumeric(4);
        // 2. 将手机获取的信息保存到缓存里,实际应用中,可以放到redis中
        String mobile = request.getParameter("mobile");
        CacheValidateCode.cacheValidateCodeHashMap.put(mobile, code);
        System.out.println("手机验证码"+code);
        return code;
    }
}

login.html 登入页,十分简单

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
</head>
<script src="https://upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.2.min.js"></script>

<body>

<form action="http://127.0.0.1:8080/login"method="post">
    <label for="username">用户名:</label>
    <input type="text" name="username" id="username">

    <label for="password">密 码:</label>
    <input type="password" name="password" id="password">
    <button type="submit">登录</button>
</form>
<form action="http://127.0.0.1:8080/mobile/form"method="post">
    <label for="mobile">手机号:</label>
    <input type="text" name="mobile" id="mobile">

    <label for="sendCode">验证码:</label>
    <input type="text" name="code" id="sendCode">
    <button type="submit">登录</button>
</form>
<button onclick="sendCode()"> 获取验证码 </button>
<script>
    function sendCode() {
        $.ajax(
            {
                type: "post",
                url: "http://127.0.0.1:8080/code/mobile",
                data: $("#mobile").serialize(),
                success: function (result) {
                    alert(result);
                }
            }
        )
    }
</script>
</body>
</html>

思路非常简单,就是定义了关于手机的验证filter,并放到security中,在通过验证码登入的时候,首先创建MobileAuthenticationToken,遍历所有的provider的时候,通过support方法获取到使用哪个provider,MobileAuthenticationProvider手机验证provider,验证手机号的验证码是否正确,如果正确就将MobileAuthenticationToken放到SecurityContextHolder中,保存在ThreadLocal变量中,该线程就能使用了,并且将MobileAuthenticationToken的authenticated设置为true,在security的最后一个拦截器FilterSecurityInterceptor判断是都已经验证过了,并且判断角色是否可以访问当前接口,
这样就是验证的整个流程,session的方式验证,在登入成功的时候token放到tomcat的内存中了,key就是sessionid,前端将session传到server时,从tomcat中获取已经验证过的token,这样就实现了登入后,其他接口可以正常访问的流程.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容