前后端分离的security角色权限实现

本案例是使用SpringBoot2.7.6+security+MyBatis-plus+Vue2+axios实现

一、security简介

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,专为Java应用程序设计。

(1)基本功能

  • 身份验证(Authentication):确定用户身份的过程。在Spring Security中,这包括确认用户是谁,并验证用户提供的凭证(如用户名和密码)是否正确。
  • 授权(Authorization):确定用户是否有权进行某个操作的过程。根据用户的身份和角色,Spring Security授予用户访问应用程序资源的权限。

(2)主要特点

  1. 全面性:Spring Security提供了全面的安全性解决方案,包括认证、授权、攻击防范(如XSS、CSRF等)和会话管理等功能。
  2. 可扩展性:提供了一系列可扩展的模块,可以根据具体需求进行选择和配置,如不同的身份验证方式、授权方式、密码编码器等。
  3. 易用性:提供了快捷配置选项和基于注解的安全控制方式,使开发人员能够更轻松地实现认证和授权等功能。
  4. 社区支持:作为Spring生态系统的一部分,Spring Security得到了广泛的社区支持和更新维护。

(3)工作原理

        Spring Security通过一系列过滤器(Filter)来保护应用程序。这些过滤器按照特定的顺序组成过滤器链,每个过滤器都有特定的责任,如身份验证、授权、防止CSRF攻击等。当用户发起请求时,请求会经过过滤器链的处理,并在处理过程中进行安全验证和授权。

(4)核心组件

  1. SecurityContextHolder:用于存储安全上下文(SecurityContext)的信息,包括当前用户的身份信息、所拥有的权限等。
  2. AuthenticationManager:负责处理用户的身份验证,接收用户提交的凭据,并使用已配置的身份验证提供程序(AuthenticationProvider)进行验证。
  3. AuthenticationProvider:实际执行身份验证的组件,从用户存储源(如数据库、LDAP等)中获取用户信息,并进行密码比对或其他验证方式。
  4. UserDetailsService:用于加载UserDetails对象的接口,通常从数据库或LDAP服务器中获取用户信息。
  5. AccessDecisionManager:用于在授权过程中进行访问决策,根据用户的认证信息、请求的URL和配置的权限规则,判断用户是否有权访问资源。

二、依赖项

主要使用到Security、mysql和gson等依赖:

<!-- security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--MySQL驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.2</version>
</dependency>
<!-- Gson: Java to Json conversion -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.9</version>
</dependency>

 其中gson是用于吧java对象转成json格式,便于响应数据。

三、配置和自定义处理器

(1)关于security的配置

        在config层中的security的配置主要有SecurityConfig核心配置类CorsConfig访问配置类

        SecurityConfig核心配置类

                在yaml配置文件中,可以自定义前端登录页面:

spring:
  security:
    # 前后端分离时自定义的security的登录页面
    loginPage: http://127.0.0.1:8081/#/login
import com.security.demo.handel.CustomAccessDeniedHandler;
import com.security.demo.handel.CustomAuthenticationFailureHandler;
import com.security.demo.handel.CustomAuthenticationSuccessHandler;
import com.security.demo.service.Impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;

/**
 * 配置security
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Value("${spring.security.loginPage}")
    private String loginPage;

    @Autowired
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;

    @Autowired
    private UserServiceImpl userServiceImpl;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 添加请求授权规则
        http.authorizeRequests()
                // 首页所有人都可以访问
                .antMatchers("/public/**").permitAll()
                // user下的所有请求,user角色权限才能访问
                .antMatchers("/user/**").hasRole("USER")
                // admin下的所有请求,ADMIN角色权限才能访问
                .antMatchers("/admin/**").hasRole("ADMIN")
                // 其他任何请求都要验证身份
                .anyRequest().authenticated();
        // 开启登录页面,即没有权限的话跳转到登录页面
        http.formLogin()
                // 登录页面
                .loginPage(loginPage)
                // 用户名的name
                .usernameParameter("user")
                // 密码的name
                .passwordParameter("pwd")
                // 处理登录的Controller
                .loginProcessingUrl("/login")
                // 验证成功处理器
                .successHandler(customAuthenticationSuccessHandler)
                // 验证失败处理器
                .failureHandler(customAuthenticationFailureHandler);
        http.csrf().disable();
        // 开启记住我功能,默认保存两周
        http.rememberMe()
                // name属性
                .rememberMeParameter("remember");
        // 开启退出登录功能
        http.logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK)); // 自定义登出成功后的处理
        // 跨域
        http.cors();
        // 权限不足处理器
        http.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler);
    }

    // 认证规则
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在新版本的SpringSecurity中新增了许多加密方法,这里使用的是BCrypt
        auth.userDetailsService(userServiceImpl).passwordEncoder(new BCryptPasswordEncoder());
    }

}

        CorsConfig访问配置类

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 *  配置security的跨域访问
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .exposedHeaders("Access-Control-Allow-Headers",
                        "Access-Control-Allow-Methods",
                        "Access-Control-Allow-Origin",
                        "Access-Control-Max-Age",
                        "X-Frame-Options")
                .maxAge(3600)
                .allowedHeaders("*");
    }

}

(2)处理器Handler

        业务开发时主要是自定义登录认证成功、登录认证失败和权限不足的处理器

        登录认证成功后的处理器CustomAuthenticationSuccessHandler,主要判断验证码是否正确和响应返回R统一返回类。CustomAuthenticationSuccessHandler类

import com.google.gson.Gson;
import com.security.demo.vo.R;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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


/**
 *  自定义的security的认证成功处理器
 */

@Component("customAuthenticationSuccessHandler")
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        R result = R.ok().setMessage("登录成功。");
        response.setContentType("application/json;charset=UTF-8");
        // 校验验证码
        String code = (String) request.getSession().getAttribute("code");
        String codeInput = request.getParameter("codeInput");
        if(code==null || codeInput==null) {
            result.setCode(501).setMessage("验证码为空");
        }else {
            if (!code.equalsIgnoreCase(codeInput)) {
                result.setCode(501).setMessage("验证码错误");
            }
        }
        Gson gson = new Gson();
        response.getWriter().write(gson.toJson(result));
    }
}

        登录验证失败处理器CustomAuthenticationFailureHandler,主要用于响应返回R统一返回类。CustomAuthenticationFailureHandler类

import com.google.gson.Gson;
import com.security.demo.vo.R;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

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

/**
 *  自定义的security的认证失败处理器
 */
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        //以返回JSON数据为例
        R result = R.error().setMessage("用户名或密码错误。");
        response.setContentType("application/json;charset=UTF-8");
        Gson gson = new Gson();
        response.getWriter().write(gson.toJson(result));
    }
}

        用户访问权限不足处理器CustomAccessDeniedHandler,主要返回权限不足信息。CustomAccessDeniedHandler类

import com.google.gson.Gson;
import com.security.demo.vo.R;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

/**
* 用户权限不足被拒绝处理器
*/
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
            throws IOException {
        R result = R.error().setMessage("权限不足");
        response.setContentType("application/json;charset=UTF-8");
        Gson gson = new Gson();
        response.getWriter().write(gson.toJson(result));
    }
}

(3)重写登录验证逻辑

        在配置类中可以自定义指定的登录验证逻辑类,一般是写在service的实现类中,要求是该登录验证逻辑类必须实现UserDetailsService接口的loadUserByUsername方法,才可以在configure的auth.userDetailsService(自定义登录验证逻辑类)指定。这里我直接在UserServiceImpl类中实现:

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.security.demo.entity.User;
import com.security.demo.mapper.UserMapper;
import com.security.demo.service.UserService;
import com.security.demo.vo.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.ArrayList;
import java.util.List;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 重写UserDetailsService的loadUserByUsername方法
     * 用于登录验证和角色授权
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = this.getUserByName(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户名为null");
        }
        // 这里需要将User转换为UserDetails,并设置角色的GrantedAuthority集合
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_"+user.getRole()));
        // 如果是admin角色,就多添加USER权限
        if(user.getRole().equals("ADMIN")){
            authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        }
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String password = encoder.encode(user.getPassword());
        return new org.springframework.security.core.userdetails.User(user.getName(), password, authorities);
    }



    /**
     *  分页条件查询用户名
     */
    @Override
    public R getUserList(String name, int current, int size) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        Page<User> page = new Page<>(current, size);
        if(name!=null && !name.isEmpty()){
            queryWrapper.like("name",name);
        }
        queryWrapper.orderByDesc("id");  // 指定根据id倒序
        Page<User> userPage = userMapper.selectPage(page, queryWrapper);
        return R.ok().data("userPage", userPage);
    }

    /**
     * 新增用户
     */
    @Override
    public R addUser(User user) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", user.getName());
        User user1 = userMapper.selectOne(queryWrapper);
        if (user1 != null) {
            return R.error().setMessage("该用户名已存在");
        }
        userMapper.insert(user);
        return R.ok().setMessage("新增成功!");
    }

    /**
     * 更新用户
     */
    @Override
    public R updateUser(User user) {
        // 根据用户名查询是否重复
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", user.getName());
        User user1 = userMapper.selectOne(queryWrapper);
        if (user1 != null && user1.getId() != user.getId()) {
            return R.error().setMessage("该用户名已存在");
        }
        // 不重复就根据id来修改用户信息
        QueryWrapper<User> queryWrapper1 = new QueryWrapper<>();
        queryWrapper1.eq("id", user.getId());
        userMapper.update(user, queryWrapper1);
        return R.ok().setMessage("更新成功!");
    }

    /**
     * 删除用户
     */
    @Override
    public R deleteUser(String name) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", name);
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            return R.error().setMessage("没有该用户");
        }
        userMapper.deleteById(user.getId());
        return R.ok().setMessage("删除成功");
    }

    /**
     * 根据用户名查询用户信息
     */
    @Override
    public User getUserByName(String name) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", name);
        return userMapper.selectOne(queryWrapper);
    }

}

四、postman测试登录接口

        使用postman测试登录login接口时要注意,由于security的限制,需要模仿表单提交,而不是常规的json数据,获取验证码后,使用x-www-form-urlencoded格式。如下:

五、前端axios注意事项

        获取验证码后,使用x-www-form-urlencoded格式的post请求如下,参数是json数据。

// 登录
    login(data){
        return axios({
            method: 'post',
            url: '/login',
            data: data,
            // 模仿表单提交
            transformRequest: [
                function (data) {
                    let ret = ''
                    for (let it in data) {
                        ret +=
                            encodeURIComponent(it) +
                            '=' +
                            encodeURIComponent(data[it]) +
                            '&'
                    }
                    return ret
                }
            ],
            headers:{
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        })
    },

六、案例完整源码

        以上是security的核心代码和调用踩坑点,完整代码请转到码云仓库查看:

=========================================================================

Gitee仓库源码:https://gitee.com/BuLiangShuai01033/security-demo

作者:不凉帅 https://blog.csdn.net/yueyue763184

=========================================================================

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值