Spring Security自定义登录认证

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: Spring Security自定义登录认证

一、前言

对于一个权限管理框架而言,无论是 Shiro 还是 Spring Security,最最核心的功能,无非就是两方面:

  • 认证
  • 授权

通俗点说,认证就是我们常说的登录,授权就是权限鉴别,看看请求是否具备相应的权限

Spring Security 支持多种不同的认证方式,这些认证方式有的是 Spring Security 自己提供的认证功能,有的是第三方标准组织制订的,主要有如下一些:

一些比较常见的认证方式:

  • HTTP BASIC authentication headers:基于IETF RFC 标准。
  • HTTP Digest authentication headers:基于IETF RFC 标准。
  • HTTP X.509 client certificate exchange:基于IETF RFC 标准。

LDAP:跨平台身份验证。

  • Form-based authentication:基于表单的身份验证。
  • Run-as authentication:用户用户临时以某一个身份登录。
  • OpenID authentication:去中心化认证。

除了这些常见的认证方式之外,一些比较冷门的认证方式,Spring Security 也提供了支持。

  • Jasig Central Authentication Service:单点登录。
  • Automatic "remember-me" authentication:记住我登录(允许一些非敏感操作)。
  • Anonymous authentication:匿名登录。

......

作为一个开放的平台,Spring Security 提供的认证机制不仅仅是上面这些。如果上面这些认证机制依然无法满足你的需求,我们也可以自己定制认证逻辑。当我们需要和一些“老破旧”的系统进行集成时,自定义认证逻辑就显得非常重要了。

二、基于内存的用户认证

security默认使用的方式就是内存级的数据源认证

1、配置文件方式

当我们项目引入Spring Security后,默认的用户定义在SecurityProperties里边,是一个静态内部类,如果要定义自己的用户名密码,必然是要去覆盖默认配置,在配置文件中配置

spring.security.user.name=admin
spring.security.user.password=admin
spring.security.user.roles=ADMIN

此时重启项目,就可以使用自己定义的用户名/密码登录了。

2、配置类中重写configure方式

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Bean
    PasswordEncoder passwordEncoder() {
        //暂时先不给密码进行加密,所以返回 NoOpPasswordEncoder 的实例
        return NoOpPasswordEncoder.getInstance();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
//如果需要配置多个用户,用 and 相连
                .withUser("123")
                .password("123").roles("admin");
    }
}

3、配置类中重写userDetailsService方式

通过重写 WebSecurityConfigurerAdapter 中的 userDetailsService 方法来提供一个 UserDetailService 实例进而配置多个用户:

@Configuration
public class SecurityConfig1 extends WebSecurityConfigurerAdapter {
 
    @Bean
    PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }
    @Override
    @Bean
    public UserDetailsService userDetailsServiceBean() throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("admin").password("admin").roles("admin").build());
        manager.createUser(User.withUsername("123").password("123").roles("user").build());
        return manager;
    }
}

4、自定义实现类方式

重写UserDetailsService接口中的loadUserByUsername方法,并在配置类里申明

import org.springframework.security.core.GrantedAuthority;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
 
/**
 * 自定义认证服务实现security中的方法
 */
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
 
   //实现security接口类userdetails唯一方法
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
 
        //这个设置一个角色
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
 
        return new User("123",getPass(),authorities);
    }
 
    //对配置的用户密码123进行加密
    public String getPass(){
       return new BCryptPasswordEncoder().encode("123");
    }
}

配置类里配置自定义的类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
 
import javax.annotation.Resource;
 
@Configuration
public class SecurityConfig1 extends WebSecurityConfigurerAdapter {
 
    //注入自己的实现类
    @Resource
    private MyUserDetailsServiceImpl userDetailsService;
    @Bean
    PasswordEncoder passwordEncoder(){
        return new  BCryptPasswordEncoder();
    }
 
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}

总结:以上四种基于内存的方式认证推荐第二种

三、基于数据库数据源

1、基于默认的数据库模型认证

UserDetailsService 都有哪些实现类

可以看到,在几个能直接使用的实现类中,除了 InMemoryUserDetailsManager 之外,还有一个 JdbcUserDetailsManager,使用 JdbcUserDetailsManager 可以让我们通过 JDBC 的方式将数据库和 Spring Security 连接起来。

JdbcUserDetailsManager 自己提供了一个数据库模型,这个数据库模型保存在如下位置:

org/springframework/security/core/userdetails/jdbc/users.ddl

这里面sql语句如下

create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);

可以看到,脚本中有一种数据类型 varchar_ignorecase,这个其实是针对 HSQLDB 数据库创建的,而我们使用的 MySQL 并不支持这种数据类型,所以这里需要大家手动调整一下数据类型,将 varchar_ignorecase 改为 varchar 即可。

修改完成后,创建数据库,执行完成后的脚本。

执行完 SQL 脚本后,我们可以看到一共创建了两张表:users 和 authorities。

  • users 表中保存用户的基本信息,包括用户名、用户密码以及账户是否可用。
  • authorities 中保存了用户的角色。
  • authorities 和 users 通过 username 关联起来。

配置完成后,接下来,我们将 InMemoryUserDetailsManager 提供的用户数据用 JdbcUserDetailsManager 代替掉,如下:

@Autowired
DataSource dataSource;
@Override
@Bean
protected UserDetailsService userDetailsService() {
    JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
    manager.setDataSource(dataSource);
    if (!manager.userExists("admin")) {
        manager.createUser(User.withUsername("admin").password("123").roles("admin").build());
    }
    if (!manager.userExists("123")) {
        manager.createUser(User.withUsername("123").password("123").roles("user").build());
    }
    return manager;
}
  • 首先构建一个 JdbcUserDetailsManager 实例。
  • 给 JdbcUserDetailsManager 实例添加一个 DataSource 对象。
  • 调用 userExists 方法判断用户是否存在,如果不存在,就创建一个新的用户出来(因为每次项目启动时这段代码都会执行,所以加一个判断,避免重复创建用户)。
  • 用户的创建方法和我们之前 InMemoryUserDetailsManager 中的创建方法基本一致

因为要连接数据库,所以还需导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

配置文件配置

spring.datasource.username=root spring.datasource.password=root spring.datasource.url=jdbc:mysql:///security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

2、自定义数据库认证

Spring Security适应系统,而非让系统适应Spring Security,是Spring Security框架开发者和使用者的共识,上面使用了 InMemoryUserDetailsManager 和 JdbcUserDetailsManager 两个UserDetailsService 实现类。生效方式也很简单,只需加入 Spring 的 IoC 容器,就会被 Spring Security自动发现并使用。自定义数据库结构实际上也仅需实现一个自定义的UserDetailsService。

UserDetailsService仅定义了一个loadUserByUsername方法,用于获取一个UserDetails对象。UserDetails对象包含了一系列在验证时会用到的信息,包括用户名、密码、权限以及其他信息

这里结合mybatis认证,需要导入的依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>
<dependency>
     <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
</dependency>

自定义类

@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
 
    @Resource
    private  UserDao userDao;
   //实现security接口类userdetails唯一方法
    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        SysUser user = userDao.getUserByUserName(userName);
        //这里查出结果如:userName = "admin" passWord="$2a$10$FsNmqBMoxqRQAzcjvF8YD.Sqh3SaSkO40FfuC.VraGuKTcTeC3wDm";密码是经过BCryptPasswordEncoder加密的
        if(user==null){
            throw new UsernameNotFoundException("用户不存在");
        }
        //这个设置一个角色
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
 
        return new User(user.getUserName(),user.getPassWord(),authorities);
    }
}

配置类

@Configuration
public class SecurityConfig1 extends WebSecurityConfigurerAdapter {
 
    //注入自己的实现类
    @Resource
    private MyUserDetailsServiceImpl userDetailsService;
 
    @Bean
    PasswordEncoder passwordEncoder(){
        return new  BCryptPasswordEncoder();
    }
 
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}

四、配置方式认证

上面我们知道了自定义获取认证数据源几种方式,下面演示通过配置类的方式认证

前后端不分离配置

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/user/login").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                //=================登录界面配置=================
                .loginPage("/user/toLoginPage")//自定义访问登录页面地址
                .loginProcessingUrl("/doLogin")//自定义登录请求的url
                .usernameParameter("name")//自定义用户名、密码变量名
                .passwordParameter("pass")
 
                //=================认证配置=================
                .successForwardUrl("/index")//认证成功始终跳转的路径.defaultSuccessUrl("/hello") redirect 重定向跳转 但是优先跳转到请求的路径 ,可以传入第二个参数true总是重定向到hello
                .failureForwardUrl("/user/toLoginPage");//认证失败 跳转 错误信息在request中,.failureUrl("/login.html")认证失败 redirect跳转 错误信息在session中
    }
}

前后端分离配置

@Configuration
public class MySecurityConfig1 extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/user/login").permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
 
                //=================登录认证=================
                .loginProcessingUrl("/doLogin")
                .usernameParameter("name")
                .passwordParameter("pass")
                .successHandler(new MyAuthenticationSuccessHandler())//登录成功 自定义逻辑处理
                .failureHandler(new MFailureHandler());//登录失败 自定义逻辑处理
 
    }
}

定义登录成功的逻辑处理器

/**
 * 定义登录成功的逻辑处理器
 */
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map<String,Object> map = new HashMap<>();
        map.put("code","200");
        map.put("msg","success");
        //认证信息
        map.put("authentication",authentication);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(map));
    }
}

自定登录失败处理器

/**
 * 自定义认证失败处理器
 */
public class MFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        Map<String,Object> map = new HashMap<>();
        map.put("code","500");
        map.put("msg","success");
        //认证信息
        map.put("exception",exception);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(map));
    }
}

五、自定义认证过滤器

Spring Security 登录请求是一个 POST 请求,但是数据传输格式是 key/value 的形式。整个项目里就只有这一个 POST 请求是这样,其他 POST 请求都是 JSON 格式的数据,如果也将登录请求写成json格式呢?

用户登录的用户名/密码是在 UsernamePasswordAuthenticationFilter 类中处理的,具体的处理代码如下

public Authentication attemptAuthentication(HttpServletRequest request,
    HttpServletResponse response) throws AuthenticationException {
  String username = obtainUsername(request);
  String password = obtainPassword(request);
    //省略
}
protected String obtainPassword(HttpServletRequest request) {
  return request.getParameter(passwordParameter);
}
protected String obtainUsername(HttpServletRequest request) {
  return request.getParameter(usernameParameter);
}

从这段代码中,我们就可以看出来为什么 Spring Security 默认是通过 key/value 的形式来传递登录参数,因为它处理的方式就是 request.getParameter。

所以我们要定义成 JSON 的,思路很简单,就是自定义来定义一个过滤器代替 UsernamePasswordAuthenticationFilter ,然后在获取参数的时候,换一种方式就行了。

自定义一个过滤器

publicclass LoginFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//首先登录请求肯定是 POST,如果不是 POST ,直接抛出异常,后面的也不处理了
        if (!request.getMethod().equals("POST")) {
            thrownew AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
//因为要在这里处理验证码,所以第二步从 session 中把已经下发过的验证码的值拿出来
        String verify_code = (String) request.getSession().getAttribute("verify_code");
//接下来通过 contentType 来判断当前请求是否通过 JSON 来传递参数,如果是通过 JSON 传递参数,则按照 JSON 的方式解析,如果不是,则调用 super.attemptAuthentication 方法,进入父类的处理逻辑中,也就是说,我们自定义的这个类,既支持 JSON 形式传递参数,也支持 key/value 形式传递参数
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
            Map<String, String> loginData = new HashMap<>();
            try {
//如果是 JSON 形式的数据,我们就通过读取 request 中的 I/O 流,将 JSON 映射到一个 Map 上
                loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
            } catch (IOException e) {
            }finally {
                String code = loginData.get("code");
//从 Map 中取出 code,先去判断验证码是否正确,如果验证码有错,则直接抛出异常。这里省略验证码的判断逻辑
                checkCode(response, code, verify_code);
            }
            String username = loginData.get(getUsernameParameter());
            String password = loginData.get(getPasswordParameter());
            if (username == null) {
                username = "";
            }
            if (password == null) {
                password = "";
            }
            username = username.trim();
//接下来从 Map 中取出 username 和 password,构造 UsernamePasswordAuthenticationToken 对象并作校验
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                    username, password);
            setDetails(request, authRequest);
            returnthis.getAuthenticationManager().authenticate(authRequest);
        } else {
            checkCode(response, request.getParameter("code"), verify_code);
            returnsuper.attemptAuthentication(request, response);
        }
    }
 
    public void checkCode(HttpServletResponse resp, String code, String verify_code) {
        if (code == null || verify_code == null || "".equals(code) || !verify_code.toLowerCase().equals(code.toLowerCase())) {
            //验证码不正确
            thrownew AuthenticationServiceException("验证码不正确");
        }
    }
}

过滤器定义完成后,接下来用我们自定义的过滤器代替默认的 UsernamePasswordAuthenticationFilter,首先我们需要提供一个 LoginFilter 的实例

@Bean
LoginFilter loginFilter() throws Exception {
    LoginFilter loginFilter = new LoginFilter();
    loginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            Hr hr = (Hr) authentication.getPrincipal();
            hr.setPassword(null);
            RespBean ok = RespBean.ok("登录成功!", hr);
            String s = new ObjectMapper().writeValueAsString(ok);
            out.write(s);
            out.flush();
            out.close();
        }
    });
    loginFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            RespBean respBean = RespBean.error(exception.getMessage());
            if (exception instanceof LockedException) {
                respBean.setMsg("账户被锁定,请联系管理员!");
            } elseif (exception instanceof CredentialsExpiredException) {
                respBean.setMsg("密码过期,请联系管理员!");
            } elseif (exception instanceof AccountExpiredException) {
                respBean.setMsg("账户过期,请联系管理员!");
            } elseif (exception instanceof DisabledException) {
                respBean.setMsg("账户被禁用,请联系管理员!");
            } elseif (exception instanceof BadCredentialsException) {
                respBean.setMsg("用户名或者密码输入错误,请重新输入!");
            }
            out.write(new ObjectMapper().writeValueAsString(respBean));
            out.flush();
            out.close();
        }
    });
    loginFilter.setAuthenticationManager(authenticationManagerBean());
    loginFilter.setFilterProcessesUrl("/doLogin");
    return loginFilter;
}

当我们代替了 UsernamePasswordAuthenticationFilter 之后,原本在 SecurityConfig#configure 方法中关于 form 表单的配置就会失效,那些失效的属性,都可以在配置 LoginFilter 实例的时候配置。

另外记得配置一个 AuthenticationManager,根据 WebSecurityConfigurerAdapter 中提供的配置即可。

FilterProcessUrl 则可以根据实际情况配置,如果不配置,默认的就是 /login

最后,我们用自定义的 LoginFilter 实例代替 UsernamePasswordAuthenticationFilter,如下

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        ...
        //省略
    http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}


相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
19天前
|
安全 Java 数据安全/隐私保护
|
22天前
|
安全 Java API
第7章 Spring Security 的 REST API 与微服务安全(2024 最新版)(上)
第7章 Spring Security 的 REST API 与微服务安全(2024 最新版)
40 0
第7章 Spring Security 的 REST API 与微服务安全(2024 最新版)(上)
|
22天前
|
存储 安全 前端开发
第6章 Spring Security 的 Web 安全性(2024 最新版)(上)
第6章 Spring Security 的 Web 安全性(2024 最新版)
55 0
|
22天前
|
安全 Java Go
第6章 Spring Security 的 Web 安全性(2024 最新版)(下)
第6章 Spring Security 的 Web 安全性(2024 最新版)
58 1
|
22天前
|
存储 安全 Java
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)(下)
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)
24 2
|
22天前
|
安全 Cloud Native Java
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)(上)
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)
29 2
|
22天前
|
存储 安全 Java
第9章 Spring Security 的测试与维护 (2024 最新版)(下)
第9章 Spring Security 的测试与维护 (2024 最新版)
21 1
|
22天前
|
安全 Java 测试技术
第9章 Spring Security 的测试与维护 (2024 最新版)(上)
第9章 Spring Security 的测试与维护 (2024 最新版)
24 0
|
22天前
|
缓存 Java 数据库
第8章 Spring Security 的常见问题与解决方案(2024 最新版)(下)
第8章 Spring Security 的常见问题与解决方案(2024 最新版)
30 0
|
22天前
|
安全 Java 数据安全/隐私保护
第8章 Spring Security 的常见问题与解决方案(2024 最新版)(上)
第8章 Spring Security 的常见问题与解决方案(2024 最新版)
34 0