SpringSecurity使用教程

一、基本使用

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,专门设计用于保护基于 Spring 的应用程序。它不仅提供了全面的安全服务,还与 Spring 框架及其生态系统(如 Spring Boot、Spring MVC 等)紧密集成,简化了安全配置和实现。而认证和授权也是SpringSecurity作为安全框架的核心功能。

1.导入依赖

XML 复制代码
        <!--实现认证与授权功能。快速设置HTTP基本认证或表单登录-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
            <version>2.2.4.RELEASE</version>
        </dependency>

2.登录界面

SpringSecurity提供了登录和退出功能,导入SpringSecurity依赖后,必须登录后才能访问

登录链接:http://localhost:8080/login

默认情况下登录账号:user,密码在项目启动时会在控制台中打印

退出链接:http://localhost:8080/logout ​​​​​​

3.配置类

SpringSecurity的配置类,通过继承WebSecurityConfigurerAdapter 注解,在内存中设置账号密码

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //配置用户信息服务
    @Bean
    public UserDetailsService userDetailsService() {
        //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build()); //拥有p1权限
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build()); //拥有p2权限
        return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
//        //密码为明文方式,因为(配置类)内存中设置的密码是明文的,后期从数据库获取密文密码就用下面一个
        return NoOpPasswordEncoder.getInstance();
//          return new BCryptPasswordEncoder(); //密文,BCryptPasswordEncoder 会对输入的密码应用 bcrypt 算法并加上随机生成的盐值,然后将得到的散列值保存到数据库中
    }
}

二、RBAC权限模型

1.简介

RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用 也是相对易用、通用权限模型。

2.建表语句

bash 复制代码
  /*Table structure for table `sys_user` */
 DROP TABLE IF EXISTS `sys_user`;
 CREATE TABLE `sys_user` (
 `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
 `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
 `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
 `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
 `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
 `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
 `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
 `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
 `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
 `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
 `create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',
 `create_time` datetime DEFAULT NULL COMMENT '创建时间',
 `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
 `update_time` datetime DEFAULT NULL COMMENT '更新时间',
 `del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
 PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
 
 
 
  /*Table structure for table `sys_user_role` */
 DROP TABLE IF EXISTS `sys_user_role`;
 CREATE TABLE `sys_user_role` (
 `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',
 `role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
 PRIMARY KEY (`user_id`,`role_id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 
 
  /*Table structure for table `sys_role` */
 DROP TABLE IF EXISTS `sys_role`;
 CREATE TABLE `sys_role` (
 `id` bigint(20) NOT NULL AUTO_INCREMENT,
 `name` varchar(128) DEFAULT NULL,
`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
 `status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
 `del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
 `create_by` bigint(200) DEFAULT NULL,
 `create_time` datetime DEFAULT NULL,
 `update_by` bigint(200) DEFAULT NULL,
 `update_time` datetime DEFAULT NULL,
 `remark` varchar(500) DEFAULT NULL COMMENT '备注',
 PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
 
 
 
  /*Table structure for table `sys_role_menu` */
 DROP TABLE IF EXISTS `sys_role_menu`;
 CREATE TABLE `sys_role_menu` (
 `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
 `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',
 PRIMARY KEY (`role_id`,`menu_id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
 
 
 
 DROP TABLE IF EXISTS `sys_menu`;
 CREATE TABLE `sys_menu` (
 `id` bigint(20) NOT NULL AUTO_INCREMENT,
 `menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
 `path` varchar(200) DEFAULT NULL COMMENT '路由地址',
 `component` varchar(255) DEFAULT NULL COMMENT '组件路径',
 `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
 `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
 `perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
 `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
 `create_by` bigint(20) DEFAULT NULL,
 `create_time` datetime DEFAULT NULL,
 `update_by` bigint(20) DEFAULT NULL,
 `update_time` datetime DEFAULT NULL,
 `del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
 `remark` varchar(500) DEFAULT NULL COMMENT '备注',
 PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';

3.查询某个用户的权限

bash 复制代码
SELECT 
DISTINCT m.`perms`
 FROM
 sys_user_role ur
 LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
 LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
 LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
 WHERE
 user_id = 2
 AND r.`status` = 0
 AND m.`status` = 0

三、实体类

1.用户表

java 复制代码
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
 import java.util.Date;
 //用户表(User)实体类
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
 @TableName(value = "sys_user")
 public class User implements Serializable {
     private static final long serialVersionUID = -40356785423868312L;//序列化版本号

     @TableId
     private Long id;//主键
     private String userName;//用户名
     private String nickName;//昵称
     private String password;//密码
     private String status;// 账号状态(0正常 1停用)
     private String email;//邮箱
     private String phonenumber;//手机号
     private String sex;// 用户性别(0男,1女,2未知)
     private String avatar;//头像
     private String userType;// 用户类型(0管理员,1普通用户)
     private Long createBy;//创建人的用户id
     private Date createTime;//创建时间
     private Long updateBy;//更新人
     private Date updateTime;//更新时间
     private Integer delFlag;//删除标志(0代表未删除,1代表已删除)
 }

2.菜单表(权限表)

java 复制代码
package com.xuexi.pojo;

 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import java.io.Serializable;
 import java.util.Date;
 //菜单(权限)表(Menu)实体类
 @TableName(value="sys_menu")
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
 @JsonInclude(JsonInclude.Include.NON_NULL)//避免json结果返回null值
 public class Menu implements Serializable {
     private static final long serialVersionUID = -54979041104113736L;
     @TableId
     private Long id;//主键
     private String menuName;//菜单名
     private String path;//路由地址
     private String component;//组件路径
     private String visible;//菜单状态(0显示 1隐藏)
     private String status;//菜单状态(0正常 1停用)
     private String perms;//权限标识
     private String icon;//菜单图标
     private Long createBy;
     private Date createTime;
     private Long updateBy;
     private Date updateTime;
     private Integer delFlag;//是否删除(0未删除 1已删除)
     private String remark;//备注
 }

3.LoginUser

通过实现UserDetails 来存储当前登录用户的信息

java 复制代码
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
    private User user;
    private List<String> permissions;

    //存储SpringSecurity所需要的权限信息的集合
    @JSONField(serialize = false) // json序列化时忽略
    private List<SimpleGrantedAuthority>  authorities;

    public LoginUser(User user,List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        authorities = new ArrayList<>();
        for(String permission : permissions){
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
            authorities.add(authority);
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

四、SpringSecurity相关配置

1.SecurityConfig 配置类

java 复制代码
import com.xuexi.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启基于注解的权限认证
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // token校验过滤器
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint; // 认证失败处理类
    @Autowired
    private AccessDeniedHandler accessDeniedHandler; // 权限不足处理类
     @Bean
     public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();//密文
     }
     @Override
     protected void configure(HttpSecurity http) throws Exception {
      http
              //关闭csrf
              .csrf().disable()
              //不通过Session获取SecurityContext
              .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //关闭session
              .and()
              .authorizeRequests() // 请求权限配置
              // 对于登录接口 允许匿名访问
              .antMatchers("/user/register").permitAll() //表示所有用户都可以访问(不管有没有登录都可以访问)
              .antMatchers("/user/login").anonymous() // 匿名访问(没有登录可以访问)
              // 除上面外的所有请求全部需要鉴权认证
              .anyRequest().authenticated();

      //将jwtAuthenticationTokenFilter过滤器添加到UsernamePasswordAuthenticationFilter过滤器之前
      http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
      //配置异常处理器
         http.exceptionHandling()
                   .authenticationEntryPoint(authenticationEntryPoint)
                   .accessDeniedHandler(accessDeniedHandler);
         http.cors();//开启SpringSecurity的跨域访问
     }
     @Bean
     @Override
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
     }
 }

1.configure配置

默认情况下,SpringSecurity提供了默认的登录界面,在前后端分离项目中,不要需要使用默认的登录界面

重写configure方法:

1.关闭csrf攻击,关闭session。csrf攻击是基于session,本项目采用jwt令牌,不需要考虑csrf攻击

2.设置允许访问的路径

3 .在用户登录之前,添加过滤器jwtAuthenticationTokenFilter

4.配置异常处理器,分为 认证异常处理器权限异常处理器

5.允许SpringSecurity跨域,后续还需要设置运行SpringBoot跨域

java 复制代码
import com.xuexi.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启基于注解的权限认证
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // token校验过滤器
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint; // 认证失败处理类
    @Autowired
    private AccessDeniedHandler accessDeniedHandler; // 权限不足处理类
     @Bean
     public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
     }
     @Override
     protected void configure(HttpSecurity http) throws Exception {
      http
              //关闭csrf
              .csrf().disable()
              //不通过Session获取SecurityContext
              .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //关闭session
              .and()
              .authorizeRequests() // 请求权限配置
              // 对于登录接口 允许匿名访问
              .antMatchers("/user/register").permitAll() //表示所有用户都可以访问(不管有没有登录都可以访问)
              .antMatchers("/user/login").anonymous() // 匿名访问(没有登录可以访问)
              // 除上面外的所有请求全部需要鉴权认证
              .anyRequest().authenticated();

      //将jwtAuthenticationTokenFilter过滤器添加到UsernamePasswordAuthenticationFilter过滤器之前
      http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
      //配置异常处理器
         http.exceptionHandling()
                   .authenticationEntryPoint(authenticationEntryPoint)
                   .accessDeniedHandler(accessDeniedHandler);
         http.cors();//开启SpringSecurity的跨域访问
     }
     @Bean
     @Override
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
     }
 }

2.AuthenticationManager 认证

在Spring Security中,AuthenticationManager 负责处理认证请求。默认情况下,Spring Security会创建并管理 AuthenticationManager 的实例,但是这个实例并不会自动暴露为Spring容器中的一个bean。因此,需要手动将其配置为一个bean。

3.权限认证具体实现

该方法在用户登录中进行使用,用户判断用户登录的账号密码是否正确

java 复制代码
@Autowired
private AuthenticationManager authenticationManager;

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);//认证

执行authenticationManager.authenticate(authenticationToken)时,会进行账号密码认证

账号认证

通过实现UserDetailsService接口重写loadUserByUsername方法进行账号认证

这个方法的主要职责是根据提供的用户名从数据库中加载用户信息,根据用户id获取用户的权限信息,而不涉及密码验证

java 复制代码
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xuexi.mapper.MenuMapper;
import com.xuexi.mapper.UserMapper;
import com.xuexi.pojo.User;
import com.xuexi.pojo.LoginUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private MenuMapper menuMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        //如果查询不到数据就通过抛出异常来给出提示
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }
        //TODO 根据用户查询权限信息 添加到LoginUser中

        //封装成UserDetails对象返回
        List<String> permissionKeyList =menuMapper.selectPermsByUserId(user.getId());//权限查询
        return new LoginUser(user,permissionKeyList);
    }
 }

权限查询

XML 复制代码
    <select id="selectPermsByUserId" resultType="java.lang.String">
        SELECT
            DISTINCT m.`perms`
        FROM
            sys_user_role ur
                LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
                LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
                LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
        WHERE
            user_id = #{id}
          AND r.`status` = 0
          AND m.`status` = 0
    </select>
密码验证

密码验证是在调用 AuthenticationManager.authenticate 方法时由Spring Security自动处理的

2.过滤器

在用户登录之前,需要 过滤器检验

1.若请求头中不含token,直接放行,进行登录

2.若请求头中含token,先对token进行解析,获取用户id。根据在id,在redis中查询该用户所具有的权限,

3.设置用户权限后放行

创建 filter包,配置过滤器

java 复制代码
import com.xuexi.config.RedisCache;
import com.xuexi.pojo.LoginUser;
import com.xuexi.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private RedisCache redisCache;
    @Override
    protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain) throws ServletException,IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis中获取用户信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        //存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

JWT工具类

导入依赖

XML 复制代码
        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>

工具类

java 复制代码
 import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.JwtBuilder;
 import io.jsonwebtoken.Jwts;
 import io.jsonwebtoken.SignatureAlgorithm;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;
 import java.util.Base64;
 import java.util.Date;
 import java.util.UUID;
 /**
 * JWT工具类
 */
 public class JwtUtil {
    //有效期为
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "sangeng";
    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }
    
    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }
    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }
    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis,String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);

        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("sg")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }
    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    
    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
     public static void main(String[] args) throws Exception {
         String jwt = createJWT("1234");
         System.out.println(jwt);
         Claims claims = parseJWT(jwt);
         System.out.println(claims);
         System.out.println(claims.getSubject());
     }
 }

3.异常处理器

导入fastjson依赖

XML 复制代码
        <!--fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>

认证失败处理器

java 复制代码
package com.xuexi.handle;

import com.alibaba.fastjson.JSON;
import com.xuexi.result.ResponseResult;
import com.xuexi.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

@Component
public class AuthenticationEntryPointImpl  implements AuthenticationEntryPoint {
    /**
     * 认证失败处理器
     * @param httpServletRequest
     * @param httpServletResponse
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        ResponseResult result = new  ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");//401
        String json = JSON.toJSONString(result);
        WebUtils.renderString(httpServletResponse,json);
    }
}

权限异常处理器

java 复制代码
import com.alibaba.fastjson.JSON;
import com.xuexi.result.ResponseResult;
import com.xuexi.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

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

@Component
 public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    /**
     * 权限异常处理器
     * @param request
     * @param response
     * @param accessDeniedException
     * @throws IOException
     * @throws ServletException
     */
     @Override
     public void handle(HttpServletRequest request, HttpServletResponse response,AccessDeniedException accessDeniedException) throws IOException, ServletException {
         ResponseResult result = new  ResponseResult(HttpStatus.FORBIDDEN.value(), "您的权限不足");
         String json = JSON.toJSONString(result);
         WebUtils.renderString(response,json);
     }

 }

返回值工具类

java 复制代码
package com.xuexi.result;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
// Java 对象序列化为 JSON 字符串时,可以确保那些值为 null 的字段不会出现在最终的 JSON 输出中。
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
public class ResponseResult<T> {
     private Integer code;
     private String msg;
     private T data;
    public ResponseResult(Integer code, String msg) {
       this.code = code;
       this.msg = msg;
    }
   public ResponseResult(Integer code, T data) {
       this.code = code;
       this.data = data;
   }
   public ResponseResult(Integer code, String msg, T data) {
     this.code = code;
     this.msg = msg;
     this.data = data;
    }
 }

客户端响应工具类

java 复制代码
import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 public class WebUtils
 {
    /**
     * 将字符串渲染到客户端
     * 
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }
 }

五、权限校验方法

注解实现

@PreAuthorize注解

**hasAuthority方法:**只能传入一个权限

**hasAnyAuthority方法:**可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源

**hasRole方法:**hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所 以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

**hasAnyRole方法:**有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以 这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

自定义权限表达式

我们可以在自己定义expression包下,创建自己的权限校验方法,然后在@PreAuthorize注解中使用自己的方法。

java 复制代码
import com.xuexi.pojo.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.List;

@Component("ex")
 public class SGExpressionRoot {
    public boolean hasAuthority(String authority){
        //获取当前用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        //判断用户权限集合中是否存在authority
        return permissions.contains(authority);
    }
 }

在SPEL表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的 hasAuthority方法

java 复制代码
    @RequestMapping("/hello")
    @PreAuthorize("@ex.hasAuthority('system:dept:list')")
    public String hello(){
        return "hello";
    }

六、登录退出功能的实现

java 复制代码
import com.xuexi.pojo.User;
import com.xuexi.result.ResponseResult;
import com.xuexi.service.LoginServcie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
 public class LoginController {
    @Autowired
    private LoginServcie loginServcie;
    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
        return loginServcie.login(user);
    }
    @RequestMapping("/user/logout")
    public ResponseResult logout(){
        return loginServcie.logout();
    }
 }

用户登录

1.对账号密码验证通过后,将用户id传入创建JWT令牌中,返回给前端。

2.登录时将用户信息存入redis中 ,当用户登录后,在过滤器中对JWT令牌进行解析,获取令牌中存入的用户id,根据用户id在redis中进行查询,判断用户是否登录。

3。若redis中不存在该用户 ,抛出异常,此时在 认证失败处理器提示:认证失败请重新登录"

**用户退出:**从SpringSecurity获取用户id,根据用户id删除reids中存储的用户信息

java 复制代码
import com.xuexi.config.RedisCache;
import com.xuexi.pojo.User;
import com.xuexi.pojo.LoginUser;
import com.xuexi.result.ResponseResult;
import com.xuexi.service.LoginServcie;
import com.xuexi.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Objects;

@Service
public class LoginServiceImpl implements LoginServcie {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;
    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);//认证
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("用户名或密码错误");
        }
        //使用userid生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        //authenticate存入redis
        redisCache.setCacheObject("login:" + userId, loginUser);
        //把token响应给前端
        HashMap<String, String> map = new HashMap<>();
        map.put("token", jwt);
        return new ResponseResult(200, "登陆成功", map);
    }
    @Override
    public ResponseResult logout() {
        //获取SecurityContextHolder中的用户id
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();
        redisCache.deleteObject("login:"+userid);
        return new ResponseResult(200,"退出成功");
    }
}

七、其他

1.reids配置类

java 复制代码
import com.xuexi.utils.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 @Configuration
 public class RedisConfig {
 @Bean
 @SuppressWarnings(value = { "unchecked", "rawtypes" })
 public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(connectionFactory);
    FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
    // 使用StringRedisSerializer来序列化和反序列化redis的key值
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(serializer);
    // Hash的key也采用StringRedisSerializer的序列化方式
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setHashValueSerializer(serializer);
    template.afterPropertiesSet();
    return template;
 }
 }

2.redis序列化工具类

XML 复制代码
        <!--fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
java 复制代码
import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.serializer.SerializerFeature;
 import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;

import java.nio.charset.Charset;
 /**
 * Redis使用FastJson序列化
 */
 public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    private Class<T> clazz;
    static{
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }
    public FastJsonRedisSerializer(Class<T> clazz){
        super();
        this.clazz = clazz;
    }
    @Override
    public byte[] serialize(T t) throws SerializationException{
        if (t == null) {
            return new byte[0];
        }
        return JSON.toJSONString(t,SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }
    @Override
    public T deserialize(byte[] bytes) throws SerializationException{
        if (bytes == null || bytes.length <= 0){
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        return JSON.parseObject(str, clazz);
    }
    protected JavaType getJavaType(Class<?> clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
 }

3.redis工具类

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
 import java.util.concurrent.TimeUnit;
 @SuppressWarnings(value = { "unchecked", "rawtypes" })
 @Component
 public class RedisCache {
    @Autowired
    public RedisTemplate redisTemplate;
    public <T> void setCacheObject(final String key, final T value){
        redisTemplate.opsForValue().set(key, value);
    }
    /**
     * 缓存基本的对象,Integer、String、实体类等
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }
    /**
     * 设置有效时间
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }
    /**
     * 设置有效时间
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit){
        return redisTemplate.expire(key, timeout, unit);
    }
    /**
     * 获得缓存的基本对象。
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key){
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }
    /**
     * 删除单个对象
     * @param key
     */
    public boolean deleteObject(final String key){
        return redisTemplate.delete(key);
    }
    /**
     * 删除集合对象
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection){
        return redisTemplate.delete(collection);
    }
    /**
     * 缓存List数据
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList){
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }
    /**
     * 获得缓存的list对象
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key){
        return redisTemplate.opsForList().range(key, 0, -1);
    }
    /**
     * 缓存Set
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet){
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext()){
            setOperation.add(it.next());
        }
        return setOperation;
    }
    /**
     * 获得缓存的set
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key){
        return redisTemplate.opsForSet().members(key);
    }
    /**
     * 缓存Map
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap){
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }
    /**
     * 获得缓存的Map
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key){
        return redisTemplate.opsForHash().entries(key);
    }
    /**
     * 往Hash中存入数据
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value){
        redisTemplate.opsForHash().put(key, hKey, value);
    }
    /**
     * 获取Hash中的数据
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey){
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }
    /**
     * 删除Hash中的数据
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey){
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }
    /**
     * 获取多个Hash中的数据
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys){
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }
    /**
     * 获得缓存的基本对象列表
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern){
        return redisTemplate.keys(pattern);
    }
 }

4.SpringBoot跨域配置类

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
 public class CorsConfig implements WebMvcConfigurer {

    /**
     * 先对SpringBoot配置,运行跨域请求
     * @param registry
     */
 @Override
 public void addCorsMappings(CorsRegistry registry) {
 // 设置允许跨域的路径

        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }
 }
相关推荐
smileNicky11 小时前
SpringBoot系列之从繁琐配置到一键启动之旅
java·spring boot·后端
柏油14 小时前
Spring @TransactionalEventListener 解读
spring boot·后端·spring
小小工匠15 小时前
Maven - Spring Boot 项目打包本地 jar 的 3 种方法
spring boot·maven·jar·system scope
板板正17 小时前
Spring Boot 整合MongoDB
spring boot·后端·mongodb
泉城老铁18 小时前
在高并发场景下,如何优化线程池参数配置
spring boot·后端·架构
泉城老铁18 小时前
Spring Boot中实现多线程6种方式,提高架构性能
spring boot·后端·spring cloud
hrrrrb19 小时前
【Java Web 快速入门】九、事务管理
java·spring boot·后端
布朗克16821 小时前
Spring Boot项目通过RestTemplate调用三方接口详细教程
java·spring boot·后端·resttemplate
IT毕设实战小研1 天前
基于Spring Boot校园二手交易平台系统设计与实现 二手交易系统 交易平台小程序
java·数据库·vue.js·spring boot·后端·小程序·课程设计
孤狼程序员1 天前
【Spring Cloud 微服务】1.Hystrix断路器
java·spring boot·spring·微服务