Spring Seccurity & Shiro

Spring Seccurity & Shiro

 JWT的原理和流程

一.流程说明:

  1. 浏览器发起请求登陆,携带用户名和密码;

  1. 服务端验证身份,根据算法,将用户标识符打包生成 token,

  1. 服务器返回token(JWT信息)给浏览器,JWT不包含敏感信息;

  1. 浏览器发起请求获取用户资料,把刚刚拿到的token一起发送给服务器;

  1. 服务器发现数据中有 token,验明正身;

  1. 服务器返回该用户的用户资料;

二.JWT消息构成

一个token分3部分,按顺序:

  • 头部(header)

1.声明类型,这里是jwt

2.声明加密的算法 通常直接使用 HMAC SHA256

  • 载荷(payload) Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据

  • 签证(signature) 对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。注意JWT对象为一个长字串,各字串之间也没有换行符,一般格式为:xxxxx.yyyyy.zzzzz 。

三.JWT 的原理

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,返回给用户,就像下面这样。

{ "姓名": "张三",

"角色": "管理员",

"到期时间": "2018年7月1日0点0分" }

以后,用户与服务端通信的时候,都要返回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

区别

(1) session 存储在服务端占用服务器资源,而 JWT 存储在客户端

(2) session 存储在 Cookie 中,存在伪造跨站请求伪造攻击的风险

(3) session 只存在一台服务器上,那么下次请求就必须请求这台服务器,不利于分布式应用

(4) 存储在客户端的 JWT 比存储在服务端的session 更具有扩展性

 Spring security的简单原理:

SpringSecurity有很多很多的拦截器,在执行流程里面主要有两个核心的拦截器

登陆验证拦截器AuthenticationProcessingFilter

资源管理拦截器AbstractSecurityInterceptor

但拦截器里面的实现需要一些组件来实现,所以就有了AuthenticationManager认证管理器、accessDecisionManager决策管理器等组件来支撑。

FilterChainProxy是一个代理,真正起作用的是各个Filter,这些Filter作为Bean被Spring管理,是Spring Security核心,各有各的职责,不直接处理认证和授权,交由认证管理器和决策管理器处理!

大概流程

认证管理

流程图解读:

1、用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。

2、然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证 。

3、认证成功后,AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。

4、SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。

授权管理

访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转到403页面、自定义页面。

SecurityConfig文件详解

HttpSecurity 和WebSecurity与Web 相关,AuthenticationManagerBuilder与验证相关

AuthenticationManagerBuilder : 用来配置全局的验证资讯,也就是AuthenticationProvider 和UserDetailsService。
WebSecurity : 用来配置全局忽略的规则,如静态资源、是否Debug、全局的HttpFirewall、SpringFilterChain 配置、privilegeEvaluator、expressionHandler、securityInterceptor,启动HTTPS等
HttpSecurity : 用来配置各种具体的验证机制规则,如OpenIDLoginConfigurer、AnonymousConfigurer、FormLoginConfigurer、HttpBasicConfigurer 等。
 

 SpringBoot 整合Seccurity认证授权

SpringSecurity+Jwt做前后端分离权限认证_jwtloginfilter_雾晴的博客-CSDN博客

第一步:引入所需maven 依赖

      <!-- security架包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--引入thymeleaf与Spring Security整合的依赖-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

   <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

第二步: 配置JWT过滤器 

所有的请求都会先经过过滤器 (除配置了白名单的) 

这里拿到请求中携带的token,将token中权限部分取出来,然后保存到Security的Authentication授权管理器中,给我们后面注入那个AuthenticationProviderImpl,

package com.shangdi.jigui.filter;


import com.shangdi.jigui.entity.LoginUser;
import com.shangdi.jigui.utils.JwtUtil;
import com.shangdi.jigui.utils.RedisCache;
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;

/**
 * <p>
 * JWT过滤器
 * </p>
 *
 * @author shangdi
 */
@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
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
         //放行
        filterChain.doFilter(request,response);
    }


}

第三步:配置SecurityConfig 

package com.shangdi.jigui.config;

import com.shangdi.jigui.filter.JwtAuthenticationTokenFilter;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) //基于注解的权限控制方案
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    public SecurityConfig() {
    }

    //配置采用哪种密码加密算法
    @Bean
    public PasswordEncoder passwordEncoder() {
//        return new BCryptPasswordEncoder();  // 使用BCrypt加密密码
//        return NoOpPasswordEncoder.getInstance(); //密码不加密
        //strength=10,即密钥的迭代次数(strength取值在4~31之间,默认为10)
        //return new BCryptPasswordEncoder(10);

        //利用工厂类PasswordEncoderFactories实现,工厂类内部采用的是委派密码编码方案.
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();

    }

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean  //暴露authenticationManager
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //登录失败的页面 //也是post请求
                // 对于登录接口 允许匿名访问
                .antMatchers("/toLogin", "/user/*", "/swagger-ui.html/**", "/swagger-resources/**", "/webjars/**", "/v2/**", "/api/**", "/doc.html").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        //允许跨域
        http.cors();
        //添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

}

创建 JWT工具类 

package com.shangdi.jigui.utils;

import io.jsonwebtoken.*;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
 * JWT工具类
 *
 * @author shangdi
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "hyj";

    public static String getUUID() {
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }

    /**
     * 生成jwt
     *
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jwt
     *
     * @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();
    }

    public static void main(String[] args) throws Exception {
        //解密
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwZGI3MjIwYTY3MzQ0NGQ2ODYyYTBjOWY3YmMxYjhkNCIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTY3ODI0MTY0MywiZXhwIjoxNjc4MjQ1MjQzfQ.8tji5vArlcdrjLujWUbMcAEPpW5eWGk1HqfSkPZaQh4";
        String subject = parseJWT(token).getSubject();
        System.out.println(subject);
        //加密
        /*String jwt = createJWT("1");
        System.out.println(jwt);*/

    }

    /**
     * 生成加密后的秘钥 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();
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secretKey) // 设置标识名
                    .parseClaimsJws(jwt)  //解析token
                    .getBody();
        } catch (ExpiredJwtException e) {
            claims = e.getClaims();
        }
        return claims;
    }


}

登录接口实现

package com.shangdi.jigui.service.impl;

import com.shangdi.jigui.entity.LoginUser;
import com.shangdi.jigui.entity.ResponseResult;
import com.shangdi.jigui.service.LoginService;
import com.shangdi.jigui.utils.JwtUtil;
import com.shangdi.jigui.utils.RedisCache;
import org.apache.catalina.User;
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.Map;

/**
 * <p>
 * 用户登录实现接口
 * </p>
 *
 */
@Service
public class LoginServiceImpl implements LoginService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;


    @Override
    public ResponseResult login(String username,String password) {
        // 用户验证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);
        // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //使用userid生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getUserId().toString();
        String jwt = JwtUtil.createJWT(userId);
        Map<String, String> map = new HashMap<>();
        map.put("token", jwt);
        //把完整的用户信息存入redis,userid作为key
        redisCache.setCacheObject("login:" + userId, loginUser);
        return new ResponseResult(200, "登录成功!!", map);
    }

    @Override
    public ResponseResult loginOut() {
        //获取SecurityContextHolder中的用户id
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        int id = loginUser.getUser().getUserId();
        redisCache.deleteObject("login:" + id);
        return new ResponseResult(200, "退出成功!!");
    }



}

实现  org.springframework.security.core.userdetails.UserDetailsService 接口

重写  loadUserByUsername 方法

import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;


/**
 * <p>
 * 用户查询接口
 * </p>
 *
 * @author shangdi
 * @since 2023-03-10
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private UserRoleService userRoleService;
    @Autowired
    private RolePermissionService rolePerService;


    @Override
    public UserDetails loadUserByUsername(String username) {
        //查询用户信息
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("user_name", username);
        //如果没有查询到用户
        User user = userMapper.selectOne(wrapper);
        if (Objects.isNull(user)) {
            throw new InternalAuthenticationServiceException("用户名不存在!!");
        }
        //定义权限列表
        Set<String> pers = new HashSet<>();
        //获取用户角色集合
        List<Role> roles = userRoleService.queryByUId(user.getUserId());
        for (Role role : roles) {
            //拿到用户具有的权限
            List<Permission> permissions = rolePerService.queryByRId(role.getRid());
            for (Permission permission : permissions) {
                pers.add(permission.getStr());
            }
        }
        return new LoginUser(user, pers);
    }

用户请求登录接口,SecurityConfig白名单放行。
效验用户名密码,效验成功,获取用户信息,如果redis缓存中有用户信息,就直接返回用户信息,
否则就查询返回用户信息并存入redis,根据用户信息生成token并返回给前端,每次请求时,JwtAuthenticationFilter
会判断是否携带token,需携带token才能放行。
SpringSecurity+JWT认证流程解析-CSDN博客

前端vue3+token实现用户认证

接收并存储后端返回的token 数据

 1.新建一个auth.js  (设置Token存储方法)

import Cookies from 'js-cookie'

const TokenKey = 'Admin-Token'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token) {
  return Cookies.set(TokenKey, token, {expires: 300})
}


export function removeToken() {
  return Cookies.remove(TokenKey)
}

2.

3.request.js

import axios from 'axios'
import {getToken} from '@/utils/auth'
import errorCode from '@/utils/errorCode'
// import {tansParams} from '@/utils/ruoyi'
import NProgress from 'nprogress'
import {ElNotification} from "element-plus";

export const baseURL = import.meta.env.VITE_APP_BASE_API;
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
  // axios中请求配置有baseURL选项,表示请求URL公共部分
  baseURL: baseURL, // 超时 默认 3000
  timeout: 5000
})

// request拦截器
service.interceptors.request.use(config => {
  NProgress.start()
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  }
  // get请求映射params参数
  if (config.method === 'get' && config.params) {
    let url = config.url + '?' + tansParams(config.params);
    url = url.slice(0, -1);
    config.params = {};
    config.url = url;
  }
  return config
}, error => {
  NProgress.done()
  Promise.reject(error)
})

// 响应拦截器
service.interceptors.response.use(res => {
  // 未设置状态码则默认成功状态
  const code = res.data.code;
  // 获取错误信息
  const msg = errorCode[code] || res.data.msg || errorCode['default']
  // 二进制数据则直接返回
  if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
    return res.data
  }
  NProgress.done()
  if (code !== 200) {
    ElNotification.error(msg)
    return Promise.reject(res.data)
  } else {
    return res;
  }
}, error => {
  console.log('err' + error)
  let {message} = error;
  if (message === "Network Error") {
    message = "后端接口连接异常";
  } else if (message.includes("timeout")) {
    message = "系统接口请求超时";
  } else if (message.includes("Request failed with status code")) {
    message = "系统接口" + message.substr(message.length - 3) + "异常";
  }
  ElNotification.error(message)
  NProgress.done()
  return Promise.reject(error)
})

export const POST = (url, data, options) => service({url, method: 'post', data, ...options});

export const DELETE = (url, params, options) => service({url, method: 'delete', params, ...options});

export const GET = (url, params, options) => service({url, method: 'get', params, ...options});

export const PUT = (url, data, options) => service({url, method: 'put', data, ...options});

export default service

请求效果如下:


 

SpringBoot 整合Shrio 认证授权

8、Shiro整合Thymeleaf_哔哩哔哩_bilibili

SpringBoot与Shiro整合详解_springboot shiro_Zero摄氏度的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值