前言

之前写的 涂涂影院管理系统 这个 demo 是基于 shiro 来鉴权的,项目前后端分离后,显然集成 Spring Security 更加方便一些,毕竟,都用 Spring 了,权限管理当然 Spring Security.

花了半天时间整理的笔记,希望能对你有所帮助。

Spring Security 一句话概述:一组 filter 过滤器链组成的权限认证。

一、加入依赖

环境:项目采用 Spring Initializr 快速构建 Spring Boot ,版本交由 spring-boot-starter-parent 管理。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

在仅仅添加完依赖的情况下,启动项目看看:

1.1 控制台打印

控制台打印了一串密码,如下图所示:

访问一下项目中的某个方法:

http://localhost:7777/tmax/videoCategory/getAll

奇怪,怎么自己跳到 /login 路径下了,而且还让登陆?

1.2 账号登录

在登陆 from 表单里输入如下:

  • 用户名:user
  • 密码:0839a4ba-c8a3-4aee-8a6e-cd19c1d0b0c1(控制台打印的)

点击 Sign in 然后跳转到了目标地址:

添加 Spring Security 依赖后,实际触发了两件事,一时将系统中所有的连接服务都保护起来, 再就是会有默认配置 form 表单认证。

二、基本原理

Spring Security的整个工作流程如下所示:

绿色认证方式可以配置, 橘黄色和蓝色的位置不可更改。

Security 有两种认证方式:

  • httpbasic
  • formLogin 默认的,如上边那种方式

同样,Security 也提供两种过滤器类:

  • UsernamePasswordAuthenticationFilter 表示表单登陆过滤器
  • BasicAuthenticationFilter 表示 httpbaic 方式登陆过滤器

图中橙色的 FilterSecurityInterceptor 是最终的过滤器,它会决定当前的请求可不可以访问Controller,判断规则放在这个里面。

当不通过时会把异常抛给在这个过滤器的前面的 ExceptionTranslationFilter 过滤器。

ExceptionTranslationFilter 接收到异常信息时,将跳转页面引导用户进行认证,如上方所示的用户登陆界面。

三、自定义认证逻辑

实际开发中是不可能使用上方 Spring Security 默认的这种方式的,如何去覆盖掉 Spring Security 默认的配置呢?

我们以:将默认的 form 认证方式改为 httpbasic 方式为例。

创建SpringSecurity自定义配置类:WebSecurityConfig.java

@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {     @Override
    protected void configure(HttpSecurity http) throws Exception {         ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
                .authorizeRequests();         registry.and()
            表单登录方式
            .formLogin()
            .permitAll()
            .and()
            .logout()
            .permitAll()
            .and()
            .authorizeRequests()
            任何请求
            .anyRequest()
            需要身份认证
            .authenticated()
            .and()
            关闭跨站请求防护
            .csrf().disable()
            前后端分离采用JWT 不需要session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    }
}

重新启动项目,已经看到修改后的 httpbasic 方式认证了。

在这里我们依然采用的默认提供的用户名 user,以及每次服务器启动自动生成的 password,那么可不可以自定义认证逻辑呢?比如采用数据库中的用户登陆?

答案是肯定的。

自定义用户认证逻辑需要了解三步:
  1. 处理用户信息获取逻辑
  2. 处理用户校验逻辑
  3. 处理密码加密解密

接下来我们来看一下这三步,然后实现自定义登陆:

3.1 处理用户信息获取逻辑

Spring Security 中用户信息获取逻辑的获取逻辑是封装在一个接口里的:UserDetailService,代码如下:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

这个接口中只有一个方法,loadUserByUsername(), 该接收一个 String 类型的 username 参数,然后返回一个 UserDetails 的对象。

那么这个方法到底是干啥的呢?

通过前台用户输入的用户名,然后去数据库存储中获取对应的用户信息,然后封装在 UserDetail 实现类里面。

封装到 UserDetail 实现类返回以后,Spring Srcurity 会拿着用户信息去做校验,如果校验通过了,就会把用户放在 session 里面,否则,抛出 UsernameNotFoundException 异常,Spring Security 捕获后做出相应的提示信息。

想要处理用户信息获取逻辑,那么我们就需要自己去实现 UserDetailsService

新建 UserDetailsServiceImpl.java

@Slf4j
@Component
public class UserDetailsServiceImpl implements UserDetailsService{     @Autowired
    private UserService userService;     /**
     * 从数据库中获取用户信息,返回一个 UserDetails 对象,
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {         通过用户名获取用户
        User user = userService.findByUsername(username);
        将 user 对象转化为 UserDetails 对象
        return new SecurityUserDetails(user);
    }
}

SecurityUserDetail.java

public class SecurityUserDetails extends User implements UserDetails {

    private static final long serialVersionUID = 1L;

    public SecurityUserDetails(User user) {

        if(user!=null) {
            this.setUsername(user.getUsername());
            this.setPassword(user.getPassword());
            this.setStatus(user.getStatus());
        }
    }     @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        理想型返回 admin 权限,可自已处理这块
        return AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
    }     /**
     * 账户是否过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }     /**
     * 是否禁用
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }     /**
     * 密码是否过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }     /**
     * 是否启用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

至此,处理用户信息获取逻辑 部分完成了,主要实现 UserDetailsService 接口的 loadUserByname 方法。

为何会用到 SecurityUserDetail 类进行转换一下?

其实完全可以直接返回一个 User 对象,但是需要注意的是,如果直接返回 User 对象的话,返回的是 security 包下的 user。

至于为何这样处理,如果返回的是 security 包下的 user,这样就失去了使用本地数据库的意义,下方自定义登陆逻辑详细说明。

再来登陆试一下:

其中 niceyoo、 为数据库用户信息,如下图为成功跳转:

3.2 处理用户校验逻辑

关于用户的校验逻辑主要包含两方面:

  1. 密码是否匹配【由Sprin Security处理,只需要告诉其密码即可】
  2. 密码是否过期、或者账户是否被冻结等

前者,已经通过实现 UserDetailsService 的 loadUserByname() 方法实现了,接下来主要看看后者。

用户密码是否过期、是否被冻结等等需要实现 UserDetails 接口:

public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();授权列表;

    String getPassword();从数据库中查询到的密码;

    String getUsername();用户输入的用户名;

    boolean isAccountNonExpired();当前账户是否过期;

    boolean isAccountNonLocked();账户是否被锁定;

    boolean isCredentialsNonExpired();账户的认证时间是否过期;

    boolean isEnabled();是账户是否有效。
}

主要看后四个方法:

1、isAccountNonExpired() 账户没有过期 返回true 表示没有过期
2、isAccountNonLocked() 账户没有锁定
3、isCredentialsNonExpired() 密码是否过期
4、isEnabled() 是否被删除

如上四个方法,皆可根据实际情况做响应处理。

3.3 处理密码加密解密

再回到 WebSecurityConfig 自定义配置类。加入:

@Autowired
private UserDetailsServiceImpl userDetailsService; @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());//加密
}

配置了这个 configure 方法以后,从前端传递过来的密码就会被加密,所以从数据库查询到的密码必须是经过加密的,而这个过程都是在用户注册的时候进行加密的。

补充:UserDetailsServiceImpl 为自定义的 UserDetailsService 实现类。

四、个性化认证流程

同样的在实际的开发中,对于用户的登录认证,不可能使用 Spring Security 自带的方式或者页面,需要自己定制适用于项目的登录流程。

Spring Security 支持用户在配置文件中配置自己的登录页面,如果用户配置了,则采用用户自己的页面,否则采用模块内置的登录页面。

WebSecurityConfig 配置类中增加 成功、失败过滤器。

@Autowired
private AuthenticationSuccessHandler successHandler; @Autowired
private AuthenticationFailHandler failHandler; @Override
protected void configure(HttpSecurity http) throws Exception {     ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
            .authorizeRequests();     registry.and()
        表单登录方式
        .formLogin()
        .permitAll()
        成功处理类
        .successHandler(successHandler)
        失败
        .failureHandler(failHandler)
        .and()
        .logout()
        .permitAll()
        .and()
        .authorizeRequests()
        任何请求
        .anyRequest()
        需要身份认证
        .authenticated()
        .and()
        关闭跨站请求防护
        .csrf().disable()
        前后端分离采用JWT 不需要session
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

在添加 AuthenticationSuccessHandler、AuthenticationFailHandler 后会帮我们自动导包,但是,既然是个性化认证流程,自然要我们自己去实现~

那我们究竟要实现什么效果呢?

自定义登陆成功处理:

自定义登陆失败处理:

为何要采用这种返回新式?

用户登录成功后,Spring Security 的默认处理方式是跳转到原来的链接上,这也是企业级开发的常见方式,但是有时候采用的是 Ajax 方式发送的请求,往往需要返回 Json 数据,如图中:登陆成功后,会把 token 返回给前台,失败时则返回失败信息。

AuthenticationSuccessHandler:

Slf4j
@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {     @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {         String username = ((UserDetails)authentication.getPrincipal()).getUsername();
        List<GrantedAuthority> authorities = (List<GrantedAuthority>) ((UserDetails)authentication.getPrincipal()).getAuthorities();
        List<String> list = new ArrayList<>();
        for(GrantedAuthority g : authorities){
            list.add(g.getAuthority());
        }
        登陆成功生成token
        String  token = UUID.randomUUID().toString().replace("-", "");
    token 需要保存至服务器一份,实现方式:redis or jwt
        输出到浏览器
        ResponseUtil.out(response, ResponseUtil.resultMap(true,200,"登录成功", token));
    }
}

SavedRequestAwareAuthenticationSuccessHandle r是 Spring Security 默认的成功处理器,默认方式是跳转。这里将认证信息作为 Json 数据进行了返回,也可以返回其他数据,这个是根据业务需求来定的,比如,上方代码在用户登陆成功后返回来 token,需要注意的是,此 token 需要在服务器备份一份,毕竟要用做下次的身份认证嘛~

AuthenticationFailHandler:

@Component
public class AuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {     @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {         ## 默认情况下,不管你是用户名不存在,密码错误,SS 都会报出 Bad credentials 异常信息
        if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"用户名或密码错误"));
        } else if (e instanceof DisabledException) {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"账户被禁用,请联系管理员"));
        } else {
            ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"登录失败,其他内部错误"));
        }
    } }

失败处理器跟成功处理此雷同。

ResponseUtil:

@Slf4j
public class ResponseUtil {     /**
     *  使用response输出JSON
     * @param response
     * @param resultMap
     */
    public static void out(HttpServletResponse response, Map<String, Object> resultMap){         ServletOutputStream out = null;
        try {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json;charset=UTF-8");
            out = response.getOutputStream();
            out.write(new Gson().toJson(resultMap).getBytes());
        } catch (Exception e) {
            log.error(e + "输出JSON出错");
        } finally{
            if(out!=null){
                try {
                    out.flush();
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

其中用到 gson 依赖:

<!-- Gson -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.5</version>
</dependency>

最后

下一篇将集成 jwt 实现用户身份认证。

SpringSecurity 整合 JWT

项目集成Spring Security的更多相关文章

  1. SpringBoot集成Spring Security

    1.Spring Security介绍 Spring security,是一个强大的和高度可定制的身份验证和访问控制框架.它是确保基于Spring的应用程序的标准 --来自官方参考手册 Spring ...

  2. SpringMVC 3.1集成Spring Security 3.1

    这篇算是一个入门文章,昨天看见有网友提问,spring mvc集成spring security 的时候出错,揣测了一下问题木有解决.我就帮忙给搭建了一个集成框架他说可以,他告诉我这样的文章网上少.今 ...

  3. 【Spring】关于Boot应用中集成Spring Security你必须了解的那些事

    Spring Security Spring Security是Spring社区的一个顶级项目,也是Spring Boot官方推荐使用的Security框架.除了常规的Authentication和A ...

  4. SpringMVC 3.2集成Spring Security 3.2

    参考:http://www.cnblogs.com/Beyond-bit/p/springmvc_and_springsecurity.html SpringMVC 3.2集成Spring Secur ...

  5. SpringBoot 集成Spring security

    Spring security作为一种安全框架,使用简单,能够很轻松的集成到springboot项目中,下面讲一下如何在SpringBoot中集成Spring Security.使用gradle项目管 ...

  6. 关于Boot应用中集成Spring Security你必须了解的那些事

    Spring Security Spring Security是Spring社区的一个顶级项目,也是Spring Boot官方推荐使用的Security框架.除了常规的Authentication和A ...

  7. Spring Boot中集成Spring Security 专题

    check to see if spring security is applied that the appropriate resources are permitted: @Configurat ...

  8. SpringBoot集成Spring Security入门体验

    一.前言 Spring Security 和 Apache Shiro 都是安全框架,为Java应用程序提供身份认证和授权. 二者区别 Spring Security:重量级安全框架 Apache S ...

  9. SpringBoot集成Spring Security(6)——登录管理

    文章目录 一.自定义认证成功.失败处理 1.1 CustomAuthenticationSuccessHandler 1.2 CustomAuthenticationFailureHandler 1. ...

随机推荐

  1. Python之路【第二十三篇】:数据库基础

    数据库的简介 数据库 数据库(database,DB)是指长期存储在计算机内的,有组织,可共享的数据的集合.数据库中的数据按一定的数学模型组织.描述和存储,具有较小的冗余,较高的数据独立性和易扩展性, ...

  2. maven系列:archetype项目模板_create-from-project

    主要介绍create-from-project插件在命令行下的使用. [第一步:生成模板项目] 新建一个maven项目,比如叫 :groupId=com.abc.demo,artifactId=com ...

  3. PB 获取或操作数据窗口语句的方法

    1.setsqlselect用法: ls_select=getsqlselect    //通过getsqlselect取得当前数据窗口的查询语句 ls_where="  "    ...

  4. Html设置问题(设置浏览器上面的图标,移动设备上面页面保存为图标)

    最近开发了一个新的项目,项目完成之后:要求把页面在移动设备上面保存为图标,通过图标直接进入系统入口(这样看着就想APP一样):刚开始通过百度直接设置了,发现有两个问题,第一.图标直接是页面的截图:第二 ...

  5. 小米9安装charles证书

    一.打开你 mac 中对应的 charles 二.点击右上角的help按钮,打开帮助弹窗 三.点击帮助弹窗中的SSL Proxying,选择save charles root certificatio ...

  6. 以yarn-client方式提交spark任务,任务一直卡在ACCEPTED状态

    问题背景 spark是以客户端的方式安装的,并没有启动spark的mesos集群,这时候的spark就相当与hive客户端. 以local模型和yarn-cluster方式提交任务,都能正确额执行,但 ...

  7. 四级CET大学词汇六级备份

    Cet6六级中要考到法庭词汇的小故事  如何安排六级考试前的一个月1.每天按照我的要求去背单词2.做四套真题,词汇部分 只做词汇 3.做personal dictionary把真题中出现的所有不认识的 ...

  8. 如何查看服务器对外的IP

    开发的时候经常会被IP受限,这是由于数据源方限制了IP,所以需要报备一下IP白名单,怎么查看自己的网络对外的IP呢? 用下面的方式最为准确: Windows上操作: 直接再浏览器访问 http://h ...

  9. 3.live555源码分析----延时队列

    live555本身是一个单进程.单线程的服务器,但是它能够完美的让多个客户端同时连接,除了使用select并发编程以外,延时队列是很重要的手段. 当连接一个客户端,进行视频帧传输的时候,是不能持续进行 ...

  10. ajax请求处理概要

    /** *不关心参数传递与参数返回的形式. */ url = ctxPath + '/ccb/xxx '; $.get(url); $.post(url); /** * 常见形式. */ var ur ...