Springboot 之 Security 前后端分离登录

本贴最后更新于 1560 天前,其中的信息可能已经时过境迁

什么是 Spring Security

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于 Spring 的应用程序的实际标准。
Spring Security 是一个框架,致力于为 Java 应用程序提供身份验证和授权。与所有 Spring 项目一样,Spring Security 的真正强大之处在于可以轻松扩展以满足自定义要求
官方网站:https://spring.io/projects/spring-security#learn

初识 Security

因为之前已经接触过其他的权限框架:shiro 所以很好入门
比较好的入门博文:
Springboot + Spring Security 实现前后端分离登录认证及权限控制
Springboot 集成 SpringSecurity 附代码
关于 SpringBoot 应用中集成 Spring Security 你必须了解的那些事
Spring Boot Security

具体代码实现

通常都是通过自定义 UserDetailsService, AuthenticationProvider, AuthenticationManager,UsernamePasswordAuthenticationFilter 其中的一种来实现的。

最常见的是通过 UserDetailsService 方式来实现,方便快捷

1 引入 Security 包

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

2 创建 Security 配置类

/**
 * @author 海加尔金鹰 www.hjljy.cn
 * @apiNote websecurtiy权限校验处理
 * @since 2020/9/11
 **/
@Configuration
@EnableWebSecurity
@EnableGlobalAuthentication
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    /**
     * 描述:
     * http方式走 Spring Security 过滤器链,在过滤器链中,给请求放行,而web方式是不走 Spring Security 过滤器链。
     * 通常http方式用于请求的放行和限制,web方式用于放行静态资源
     **/
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                //用于配置直接放行的请求
                .antMatchers("/login").permitAll()
                //其余请求都需要验证
                .anyRequest().authenticated()
                //授权码模式需要 会弹出默认自带的登录框
                .and().httpBasic()
                //禁用跨站伪造
                .and().csrf().disable();
                //如果项目没有前后端分离,还可以通过 formlogin配置登录相关的页面和请求处理
        // 使用自定义的认证过滤器
        // http.addFilterBefore(new  MyLoginFilter(authenticationManager()),UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 描述: 静态资源放行,这里的放行,是不走 Spring Security 过滤器链
     **/
    @Override
    public void configure(WebSecurity web) {
        // 可以直接访问的静态数据
        web.ignoring()
                .antMatchers("/css/**")
                .antMatchers("/404.html")
                .antMatchers("/500.html")
                .antMatchers("/html/**")
                .antMatchers("/js/**");
    }

    /**
     * 描述:设置授权处理相关的具体类以及加密方式
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        // 设置不隐藏 未找到用户异常
        provider.setHideUserNotFoundExceptions(true);
        // 用户认证service - 查询数据库的逻辑
        provider.setUserDetailsService(userDetailsService());
        // 设置密码加密算法
        provider.setPasswordEncoder(passwordEncoder());
        auth.authenticationProvider(provider);
    }

    /**
     * 描述: 通过自定义的UserDetailsService 来实现查询数据库用户数据
     **/
    @Override
    @Bean
    protected UserDetailsService userDetailsService() {
        return new UserDetailsServiceImpl();
    }

    /**
     * 描述: 密码加密算法 BCrypt 推荐使用
     **/
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 描述: 注入AuthenticationManager管理器
     **/
    @Override
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
}

3 创建 UserInfo 类

/**
 * @author 海加尔金鹰
 * @apiNote 用户信息类 主要是提供给验证框架使用,并将用户基本信息保存到这个类
 * @since 2020/9/11
 **/
public class UserInfo extends User {

    private static final long serialVersionUID = 1L;

    /**
     * 描述: 可以添加自定义的用户属性
     * 用户邮箱
     **/
    private String email;
    /**
     * 描述: 用户ID
     **/
    private String userId;

    public UserInfo(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }
    public UserInfo(String username, String password, String userId,Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
        this.userId=userId;
    }

    public UserInfo(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }
}

4 实现 UserDetailsService

/**
 * @author 海加尔金鹰
 * @apiNote 用户具体验证类
 * @since 2020/9/11
 **/
public class UserDetailsServiceImpl implements UserDetailsService {
    /**
     * 这里根据传进来的用户账号进行用户信息的构建
     * 通常的做法是
     *  1 根据username查询数据库对应的用户信息
     *  2 根据用户信息查询出用户权限信息  例如菜单添加权限  sys:menu:add
     *  3 根据用户账号,密码,权限构建对应的UserDetails对象返回
     * 这里实际上是没有进行用户认证功能的,真正的验证是在UsernamePasswordAuthenticationFilter对象当中
     * UsernamePasswordAuthenticationFilter对象会自动根据前端传入的账号信息和UserDetails对象对比进行账号的验证
     * 通常情况下,已经满足常见的使用常见,不过如果有特殊需求,需要使用自己实现的具体认证方式,可以继承UsernamePasswordAuthenticationFilter对象
     * 重写attemptAuthentication 方法和successfulAuthentication方法
     * 最后在WebSecurityConfiguration里面添加自己的过滤器即可
     * @param username 用户账号
     * @return UserInfo
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //TODO 当前使用测试数据进行测试 需要修改成实际的业务逻辑处理
        //  不限制用户账号。只要密码是123456就可以通过验证 并添加权限
        String password = SecurityUtils.encryptPassword("123456");
        SimpleGrantedAuthority authority = new SimpleGrantedAuthority("sys:menu:add");
        List<GrantedAuthority> authorities =new ArrayList<>();
        authorities.add(authority);
        UserInfo userInfo =new UserInfo(username,password,authorities);
        userInfo.setEmail("hjljy@outlook.com");
        userInfo.setUserId("11111111");
        return userInfo;
    }
}

5 提供前端一个登录接口

由于项目进行前后端分离以及 Security 自带的登录界面不美观,所以需要提供一个登录接口给前端调用,该接口需要在配置当中放行,未授权访问需要授权的请求时,会返回 401 或者 403 状态码,前端可以根据这个进行路由提示处理

@RestController
public class LoginController {


    @Autowired
    AuthenticationManager authenticationManager;

    @PostMapping(value = "login")
    public ResultInfo login(@RequestBody Map<String,String> params)  {
        UserInfo userInfo = SecurityUtils.login(params.get("username"), params.get("password"), authenticationManager);
        return ResultInfo.success(userInfo);
    }
}

6 SecurityUtils 工具类

@Slf4j
public class SecurityUtils {

    /**
     * 描述根据账号密码进行调用security进行认证授权 主动调
     * 用AuthenticationManager的authenticate方法实现
     * 授权成功后将用户信息存入SecurityContext当中
     * @param username 用户名
     * @param password 密码
     * @param authenticationManager 认证授权管理器,
     * @see  AuthenticationManager
     * @return UserInfo  用户信息
     */
    public static UserInfo login(String username, String password, AuthenticationManager authenticationManager) throws AuthenticationException {
        //使用security框架自带的验证token生成器  也可以自定义。
        UsernamePasswordAuthenticationToken token =new UsernamePasswordAuthenticationToken(username,password );
        Authentication authenticate = authenticationManager.authenticate(token);
        SecurityContextHolder.getContext().setAuthentication(authenticate);
        UserInfo userInfo = (UserInfo) authenticate.getPrincipal();
        return userInfo;
    }

    /**
     * 获取当前登录的所有认证信息
     * @return
     */
    public static Authentication getAuthentication(){
        SecurityContext context = SecurityContextHolder.getContext();
        return context.getAuthentication();
    }

    /**
     * 获取当前登录用户信息
     * @return
     */
    public static UserInfo getUserInfo(){
        Authentication authentication = getAuthentication();
        if(authentication!=null){
            Object principal = authentication.getPrincipal();
            if(principal!=null){
                UserInfo userInfo = (UserInfo) authentication.getPrincipal();
                return userInfo;
            }
        }
        throw new BusinessException();
    }

    /**
     * 获取当前登录用户ID
     * @return
     */
    public static String getUserId(){
        UserInfo userInfo = getUserInfo();
        return userInfo.getUserId();
    }

    /**
     * 生成BCryptPasswordEncoder密码
     *
     * @param password 密码
     * @return 加密字符串
     */
    public static String encryptPassword(String password) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.encode(password);
    }
}

7 登录验证

上述流程完毕之后,可以通过工具发送 HTTP 测试一下登录接口

POST http://localhost:8090/login
Content-Type: application/json

{
  "username": "测试账号",
  "password": 123456
}

成功返回账号信息

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "password": null,
    "username": "测试账号",
    "authorities": [
      {
        "authority": "sys:menu:add"
      }
    ],
    "accountNonExpired": true,
    "accountNonLocked": true,
    "credentialsNonExpired": true,
    "enabled": true,
    "email": "hjljy@outlook.com",
    "userId": "11111111"
  }
}

总结

总的来说入门还是很简单的,网上的资料也比较多,但是大多数的前后端分离都是自定义登录界面,不是接口分离。还有就是基本上 security 会搭配 oauth2 使用进行权限验证。

  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    943 引用 • 1460 回帖 • 3 关注
  • Security
    9 引用 • 15 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...