«

SpringBoot整合SpringScenery+JWT++Redis+Mybatis-Plus实现权限管理

Qihan 发布于 阅读:1001 文章


注意我的SpringBoot版本为2.6.4
大于springboot2.7版本之后的SpringScenery配置类方法不太一样

先导入pom.xml

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

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.8</version>
        </dependency>
        <!-- https://qihan.tech/tutorial/3.html -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--NoSql 非关系数据库  Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>

        <!-- jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>

首先配置下MySQL数据库和Redis数据库
application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ssm?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: gaoqihan
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      mysql:
        usePingMethod: false
  redis:
    host: 127.0.0.1
    port: 6379
#  main:
#    allow-circular-references: true   # 允许循环注入
jwt:
  # 为JWT基础信息加密和解密的密钥,长度需要大于等于43
  # 在实际生产中通常不直接写在配置文件里面。而是通过应用的启动参数传递,并且需要定期修改
  secret: oQZSeguYloAPAmKwvKqqnifiQatxMEPNOvtwPsCLasd
  # JWT令牌的有效时间,单位秒,我这边设置一天
  expiration: 86400
  header: Authorization
server:
  port: 8080
# 存在redis中的用户数据的有效时间,单位秒,我这边设置一天

以上仅做参考 具体的看自己的数据库配置
SpringBoot整合Redis部分 看本站地址https://qihan.tech/tutorial/4.html
还有jwt工具类 看本站地址 https://qihan.tech/tutorial/13.html
这边就不多加描述了

接下来我们要创建两个类
第一个我就命名为 JwtAccessDeniedHandler吧,实现接口 AccessDeniedHandler

具体代码如下

@Slf4j
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.info("用户访问没有授权资源:{}",accessDeniedException.getMessage());

        response.setContentType("application/json;charset=utf-8");
        response.setCharacterEncoding("utf-8");
        PrintWriter out = response.getWriter();
        out.write(JSONObject.toJSONString(new Result(Code.BUSINESS_ERR,"权限不足!")));
        out.flush();

    }
}

第二个类我将其命名为 JwtAuthenticationEntryPoint 实现接口AuthenticationEntryPoint
主要是在用户在访问受保护资源时被拒绝而抛出的异常 代码如下

@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        log.info("用户访问资源没有携带正确的token:{}",authException.getMessage());
        response.setContentType("application/json;charset=utf-8");
        response.setCharacterEncoding("utf-8");
        PrintWriter out = response.getWriter();
        out.write(JSONObject.toJSONString(new Result(Code.BUSINESS_ERR,"你还没有登录!")));
        out.flush();
    }
}

接下来要做一个SpringScenery专用实体类 需要继承UserDetails,原本SpringScenery也是有提供一个User类的,可以看自己需求决定是否使用自己的。其中Users类型是对应我数据库的实体类,这个根据自己需要创建

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails, CredentialsContainer{

    private Users users;
    private Set<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

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

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

    //返回当前账户是否未过期,返回true表示认证成功,返回false代表过期了。如果在系统中没有关于这个的逻辑,可以永远返回true
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //账户是否被锁定,返回当前账户是否未锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //返回当前账户凭证(如密码)是否未过期,因为有些安全级别比较高的网站可能会要求用户三十天或固定时间去修改密码。
    // 这个方法可以告诉SpringSecurity密码是否过期了。
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //返回当前账户是否可用,表示用户是否被删除了
    @Override
    public boolean isEnabled() {
        return true;
    }

    //这个方法是调用后进行数据擦除工作保证数据安全
    @Override
    public void eraseCredentials() {
        this.users.setPassword("");
    }

}

Users类 根据自己的数据库写

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class Users {
    private String ID;

    @TableField("UserName")
    private String UserName;
    @TableField("Password")
    private String Password;
}

定义一个UserMapper

@Mapper
public interface UserMapper extends BaseMapper<Users> {
}

现在就可以写自己的登入验证条件了
新建MyUserDetailsService类 实现UserDetailsService接口
具体代码如下

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询数据库进行
        QueryWrapper<Users> wrapper = new QueryWrapper<>();
        wrapper.eq("UserName", username);
        Users users = userMapper.selectOne(wrapper);
        //数据库不存在
        if (null == users) {
            throw new UsernameNotFoundException("用户名不存在!!");
        }
        //给定权限
        // AuthorityUtils.commaSeparatedStringToAuthorityList(“”) 来创建 authorities 集合对象的。参数是一个字符串,多个权限使用逗号分隔。
        //List<GrantedAuthority> authList = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_vip1,ROLE_vip2");
        // commaSeparatedStringToAuthorityList放入角色时需要加前缀ROLE_,而在controller使用时不需要加ROLE_前缀

        Set<GrantedAuthority> authorities=new HashSet<>();
        GrantedAuthority authority=new SimpleGrantedAuthority("ROLE_vip1");//注意这里根据你自己从数据库提取出来的权限  需要加ROLE_
        authorities.add(authority);

        //使用加密
        users.setPassword(new BCryptPasswordEncoder().encode(users.getPassword()));

        //有一个springsecurity自带的User是已经封装好的UserDetails  我选择自定义一个类

        return new LoginUser(users, authorities);
    }
}

再新建一个 JwtAuthenticationFilter类 实现OncePerRequestFilter接口 作为SpringScenery的自定义过滤链
具体代码如下

@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String token = request.getHeader("token");

        log.info("header token:{}", token);
        //如果请求头中有token,则进行解析,并且设置认证信息
        if (token != null && token.trim().length() > 0) {
            //根据token获取用户名
            String username = jwtUtil.getSubjectFromToken(token);

            // 验证username,如果验证合法则保存到SecurityContextHolder
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = (LoginUser)redisUtil.get(username);
                // JWT验证通过,使用Spring Security 管理
                if (jwtUtil.validateToken(token, userDetails)) {
                    //加载用户、角色、权限信息
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        //如果请求头中没有Authorization信息则直接放行
        filterChain.doFilter(request, response);
    }
}

最后新建securityConfig 配置类 具体代码如下

@Configuration
//AOP:拦截器
//@EnableWebSecurity
public class securityConfig extends WebSecurityConfigurerAdapter {

    //创建BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Autowired
    private JwtAccessDeniedHandler jwtaccessDeniedHandler;

    @Autowired
    private JwtAuthenticationEntryPoint jwtauthenticationEntryPoint;

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //继承并且重写

        // 禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //设置拦截规则,首页所有人都可以访问   vip页需对应的权限
        //请求授权的规则
        http.authorizeRequests().antMatchers("/login").permitAll()
                .antMatchers("/vip1").hasRole("vip1")
                .antMatchers("/vip2").hasRole("vip2")
                .antMatchers("/vip3").hasRole("vip3")
                .anyRequest().authenticated()
        ;

        //hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。
        //hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_** 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_** 这个前缀才可以。
        //hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_** 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_** 这个前缀才可以。

        //csrf是通过cookie伪造跨站 网站攻击 但是前后端分离是使用token 就不需要csrf
        http.csrf().disable();//关闭csrf功能   加入这段后http.logout()就不会跳转到他特定的提示退出页面 而是直接退出

        //用户访问没有授权资源
        http.exceptionHandling().accessDeniedHandler(jwtaccessDeniedHandler);
        //授权错误信息处理
        //用户访问资源没有携带正确的token
        http.exceptionHandling().authenticationEntryPoint(jwtauthenticationEntryPoint);

        // 使用自己定义的拦截机制验证请求是否正确,拦截jwt
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        //允许跨域
        http.cors();

    }

    //AuthenticationManager接口:定义了认证Authentication的方法
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    //用户认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());

        //withUsers是用户名  .password()密码  .roles()是所对应的权限  可以使用.and()无限添加用户
        //这里是强制需要使用密码加密的 所以.passwordEncoder(new BCryptPasswordEncoder())是添加加密类
        // new BCryptPasswordEncoder().encode("gaoqihan") 是加密方法
    }
}

最最最后 登入的方法
具体代码如下

@Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private AuthenticationManager authenticationManager;

    @RequestMapping("/login")
    public Result login(String username,String password)  {

        //通过类名可以的看出来,用户名密码方式进行认证。就是我们见的最多的认证方式通过用户名密码进行登录
        UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(username,password);
        Authentication authentication=authenticationManager.authenticate(authenticationToken);

        LoginUser loginUser =(LoginUser) authentication.getPrincipal();

        if (!redisUtil.set(username,loginUser,jwtUtil.expiration))
        {
            throw new SystemException("系统数据库异常!", Code.SYSTEM_ERR);
        }

        String token=jwtUtil.generateToken(loginUser);

        return new Result(Code.QUERY_OK,token,"登录成功!");

    }

原文地址https://qihan.tech/tutorial/3.html

java redis springboot jwt token mysql SpringScenery Mybatis-Plus

推荐阅读: