Security实现前后端分离

news2024/12/23 14:49:19

Security实现前后端分离

说明

​ 上一篇和上上一篇我大致介绍了一下security基础使用和oauth2的一些流程,这里在深入了解一些相关的配置项。

​ 首先我们在梳理一下相关概念,首先基本的security是负责用户认证这这一环节,总而言之就是用户使用用户名密码登录后可以访问后端接口;然后引入OAuth2相关依赖之后,就有了授权服务器,客户端,资源服务器这三个概念,而且最新的官方依赖也是按照这三个概念提供统一的依赖引入,而之前的security基本认证就属于授权服务器的一个环节。

问题

1、首先security基本的认证方式中用户名密码的传递似乎不太安全

2、授权服务器,客户端,资源服务器相关流程虽然走了一遍,但是都是使用的基础配置,大部分基础配置对于token、用户登录状态数据的存储、或者用户信息的存储都是基于内存

3、最好能够有一些更实际的应用场景

注意:以下示例或配置基于security 5.8.0相关版本

JWT

什么是JWT

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。 官网: https://jwt.io/ 标准: https://tools.ietf.org/html/rfc7519

JWT令牌的优点:

  1. jwt基于json,非常方便解析。
  2. 可以在令牌中自定义丰富的内容,易扩展。
  3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
  4. 资源服务使用JWT可不依赖授权服务即可完成授权。

缺点:

​ JWT令牌较长,占存储空间比较大。

JWT组成

一个JWT实际上就是一个字符串,它由三部分组成,头部(header)、载荷(payload)与签名(signature)。

这里以oauth2 授权服务器端返回的token做为示例,你可以将这个token在其官网首页进行解析

一下是一个示例,三部分会进行base64加密,然后用.号分隔,其中签名部分解密还需要秘钥,具体后面详细介绍

eyJraWQiOiJiY2Q4MmUxMC04MTdhLTQ3ZTQtODVlZi02OTUwNTU3NzA3ODAiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtZXNzYWdpbmctY2xpZW50IiwiYXVkIjoibWVzc2FnaW5nLWNsaWVudCIsIm5iZiI6MTY2OTUzOTk3Niwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsIm1lc3NhZ2UucmVhZCIsIm1lc3NhZ2Uud3JpdGUiXSwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MDgwIiwiZXhwIjoxNjY5NTQwMjc2LCJpYXQiOjE2Njk1Mzk5NzZ9.OS0gIJ28bl591DCB8K9dgLfFfqm--uw5OE3pbD20cXpswJigIw5wowyuUPZhjTauwFIMtRiITNLJNqt492X1paSU7GO-Y6fcRq7cNA9q8lF5GDsReuYg1h-RP5EV5X0SOFBsKHxcU9Lxe_-ujcjlyje5sthNc_kq6btn7bbd2-w01VotxTxNQwZtZpSIt-VHmFLN86YQUrRi6NA4TIiUxkyr23ik9UDtn-lW22AQyEJBKfQ1EUOuBKCv2llEKBN9VXUTZbf509RAT0ctwuh4fdTcHOs1Z5TaGqHxSsn_Pc_-BjN-c6dWrBzlsaqWHELgtmpSUZnZ2rFkowCBrYFUpw

逆向对三部分进行base64解密,可以得到以下三部分

头部(header)

头部用于描述关于该JWT的最基本的信息:类型(即JWT)以及签名所用的算法(如HMACSHA256或RSA)等。

这也可以被表示成一个JSON对象:

{
  "kid": "bcd82e10-817a-47e4-85ef-695055770780",
  "alg": "RS256"
}

这里的签名算法是RSA256,会有一对公私钥。

载荷(payload)

第二部分是载荷,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:

  • 标准中注册的声明(建议但不强制使用)

iss: jwt签发者

sub: jwt所面向的用户

aud: 接收jwt的一方

exp: jwt的过期时间,这个过期时间必须要大于签发时间

nbf: 定义在什么时间之前,该jwt都是不可用的.

iat: jwt的签发时间

jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

  • 公共的声明 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
  • 私有的声明 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
{
  "sub": "messaging-client",
  "aud": "messaging-client",
  "nbf": 1669539976,
  "scope": [
    "openid",
    "profile",
    "message.read",
    "message.write"
  ],
  "iss": "http://127.0.0.1:8080",
  "exp": 1669540276,
  "iat": 1669539976
}

签名(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret(盐,一定要保密)

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分:

JWT使用

在其官网界面又不同类型的实现,以便我们使用,就java而言,jjwt对JWT相关标准的实现更全面,所以我们可以通过jjwt来使用JWT

https://jwt.io/libraries

依赖

        <!--JWT依赖 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

测试类

public class JWTTest {

    public static void main(String[] args) {
        //创建一个JwtBuilder对象
        JwtBuilder jwtBuilder = Jwts.builder()
                //声明的标识{"jti":"666"}
                .setId("666")
                //主体,用户{"sub":"Fox"}
                .setSubject("sry")
                //签发日期{"ita":"xxxxxx"}
                .setIssuedAt(new Date())
                //签名手段,参数1:算法,参数2:盐,有多种算法
                .signWith(SignatureAlgorithm.HS256, "123123");
        //获取token
        String token = jwtBuilder.compact();
        System.out.println(token);

        //三部分的base64解密
        System.out.println("=========");
        String[] split = token.split("\\.");
        System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
        System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
        //无法解密
        System.out.println(Base64Codec.BASE64.decodeToString(split[2]));


        Jws<Claims> claimsJws = Jwts.parser().setSigningKey("123123").parseClaimsJws(token);
        Claims body = claimsJws.getBody();
        System.out.println(body.getId());
        System.out.println(body.getSubject());
        System.out.println(body.getIssuer());

        String signature = claimsJws.getSignature();
        System.out.println(signature);
        System.out.println("结束");
    }
}
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiJzcnkiLCJpYXQiOjE2Njk1NTY0ODd9.R8pLjaHmvAXy_MUGV1mIJ6_aG5etyicZtEWN8hAK1G8
=========
{"alg":"HS256"}
{"jti":"666","sub":"sry","iat":1669556487}
G�K����1A��b	项zܢq�DX�! �F
666
sry
null
R8pLjaHmvAXy_MUGV1mIJ6_aG5etyicZtEWN8hAK1G8
过期校验

还是上述代码,添加一个失效时间设置,然后打断点,等个分钟即可

                // 设置失效时间
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 1000))
Exception in thread "main" io.jsonwebtoken.ExpiredJwtException: JWT expired at 2022-11-27T21:48:44Z. Current time: 2022-11-27T21:49:34Z, a difference of 50244 milliseconds.  Allowed clock skew: 0 milliseconds.
自定义属性
                // 自定义属性
                .claim("role","admin")
                .claim("sex","1")
                // 自定义属性方式二
                .addClaims(new HashMap<>())

Security前后端分离

密码加密传输

Security默认的密码处理方式

官方文档:https://docs.spring.io/spring-security/reference/5.8/servlet/authentication/passwords/input.html

from: 默认是明文密码

basic: 密码和明文会放在请求头中,并且使用base64加密,BasicAuthenticationFilter 中会解密出明文,然后进行下一步认证

        // 开启httpbasic,密码和用户名会使用:分隔,然后base64加密传到后端
        http.httpBasic().realmName("Realm");

Digest:能够处理HTTP头中显示的摘要式身份验证凭据 ,DigestAuthenticationFilter中有相关处理逻辑

您不应该在现代应用程序中使用Digest,因为它被认为是不安全的。最明显的问题是,必须以明文、加密或MD5格式存储密码。所有这些存储格式都被认为是不安全的。相反,您应该使用单向自适应密码散列(即bcrypt、pbkdf2、scrypt等)。

总之似乎都不太适合

怎么加密密码

泄露渠道:

  • 数据库被“偷”
  • 服务器被入侵
  • 通讯被窃听
  • 内部人员泄露数据
  • 撞库

有了早些年处于热搜的“CSDN几百万用户数据泄露”、“京东账号密码数据库泄露”、“12306数据库泄露”事件,业界终于认识到数据库的“不太安全性”,以及做出了一些基本的防御措施,比如:

  • 严禁密码明文存储(防泄露)
  • 单向变换(防泄露)
  • 密码本身复杂度要求(防猜解)
  • “加盐”(防猜解)

实际应用中甚至有专门的安全键盘提供商,可以专门处理密码加密问题,然后加解密就使用他们的工具包即可,当然大部分场景下大概要就应该没这么高。

1、如果你整个流程中不需要知道用户的密码明文,做一些校验或者特殊处理,可以前端直接校验密码复杂度,然后对明文进行加盐,加盐后使用MD5加密传到后端,盐值没有特殊需求的话可以使用固定值,或者一个和用户关联的不会修改的值。

2、比如你想后端进行校验和处理的话,可以前端使用非对称加密(sm2 rsa)公钥进行加密,为了安全,加密数据可以包含一些请求内容的签名和时间戳,以便后端进行一些校验。后端解密后,在对密码进行加盐,如果已经生成了用户id的话,可以使用用户id作为盐值。或者直接使用SCryptPasswordEncoder和BCryptPasswordEncoder加密后存储到数据库

当然,就登录而言,还有一系列的验证环节,最好是可以使用https,然后校验码,ip,验证手机号等。

这部分我这里就不搞了。

代码

处理登录请求的过滤器

过滤器直接继承了UsernamePasswordAuthenticationFilter,这样的话,我们在配置类中进行配置的时候,部分配置我们可以沿用Security的默认配置,并且和其默认配置一样支持修改,比如登录地址,登录参数名称

1、这里由于重写了successfulAuthentication和unsuccessfulAuthentication方法,所以登录失败处理器和登录成功处理器配置应该就没用了

2、总之和UsernamePasswordAuthenticationFilter样,这个过滤器只支持x-www-form-urlencoded方式提交请求参数

3、在这里你可以实现一些登录错误次数统计登录成功或失败日志记录的功能

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private boolean postOnly = true;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, boolean postOnly) {
        super(authenticationManager);
        this.postOnly = postOnly;
    }

    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String username = obtainUsername(request);
        username = (username != null) ? username.trim() : "";
        String password = obtainPassword(request);
        password = (password != null) ? password : "";
        UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(username,
                password);
        return this.getAuthenticationManager().authenticate(token);
    }

    /**
     * 原有逻辑中登录成功后,转发到登录成功处理器,这里重写之后,就没有登录成功处理器什么事了
     * */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        User user = (User) authResult.getPrincipal();
        // 从User中获取权限信息
        Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
        // 创建Token
        String token = JwtTokenUtil.createToken(user.getUsername(), authorities.toString());
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        // 在请求头里返回创建成功的token
        // 设置请求头为带有"Bearer "前缀的token字符串
        response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write("登录成功");
    }

    /**
     * 原有逻辑中登录失败后,转发到登录失败处理器,这里重写之后,就没有登录失败处理器什么事了
     * */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        String returnData="";
        if (failed instanceof AccountExpiredException) {
            returnData="账号过期";
        }else if (failed instanceof BadCredentialsException) {
            returnData="密码错误";
        }else if (failed instanceof CredentialsExpiredException) {
            returnData="密码过期";
        }else if (failed instanceof DisabledException) {
            returnData="账号不可用";
        }else if (failed instanceof LockedException) {
            returnData="账号锁定";
        }else if (failed instanceof InternalAuthenticationServiceException) {
            returnData="用户不存在";
        }else{
            returnData="未知异常";
        }

        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(returnData);
    }
}

UserDetailsService实现

认证管理器会从UserDetailsService的具体实现中去获取用户信息,这里是从数据库获取用户信息

你可以实现一些针对用户信息的验证

1、比如用户的状态,密码错误次数,密码过期,你可以抛出一些认证异常,异常你可以参考AuthenticationException中的相关实现

package cn.sry1201.security.service;

import cn.sry1201.security.domain.User;
import cn.sry1201.security.mapper.PermissionMapper;
import cn.sry1201.security.mapper.UserMapper;
import org.apache.commons.lang3.StringUtils;
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;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PermissionMapper permissionMapper;

    public User getByUsername(String username) {
        return userMapper.getByUsername(username);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //从mysql查询用户
        User user = getByUsername(username);
        if(user!=null){

            List<String> roles = permissionMapper.getRoles(user.getId());
            String rolesStr = StringUtils.join(roles, ",");
            // 封装成UserDetails的实现类,用户名,权限,密码
            UserDetails userDetails = org.springframework.security.core.userdetails.User.withUsername(user.getUsername())
                    .password(user.getPassword()).roles(rolesStr).build();
            return userDetails;
        }else {
            throw new UsernameNotFoundException("用户名不存在");
        }

    }
}

解析token并认证

主要是从token中获取权限

@Component
public class JWTAuthorizationFilter extends OncePerRequestFilter {


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER);
        // 若请求头中没有Authorization信息 或是Authorization不以Bearer开头 则直接放行
        if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)){
            filterChain.doFilter(request, response);
            return;
        }
        // 从请求头中解析出认证信息
        UsernamePasswordAuthenticationToken authentication = getAuthentication(tokenHeader);

        // 若请求头中有token 则调用下面的方法进行解析 并设置认证信息
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
    }

    /**
     * 从token中获取用户信息并新建一个token
     *
     * @param tokenHeader 字符串形式的Token请求头
     * @return 带用户名和密码以及权限的Authentication
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        // 去掉前缀 获取Token字符串
        String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX, "");
        // 从Token中解密获取用户名
         String username = JwtTokenUtil.getUsername(token);
        // 从Token中解密获取用户角色
        String role = JwtTokenUtil.getUserRole(token);
        // 将[ROLE_XXX,ROLE_YYY]格式的角色字符串转换为数组
        String[] roles = StringUtils.strip(role, "[]").split(", ");
        Collection<SimpleGrantedAuthority> authorities=new ArrayList<>();
        for (String s:roles)
        {
            authorities.add(new SimpleGrantedAuthority(s));
        }
        if (username != null)
        {
            return new UsernamePasswordAuthenticationToken(username, null,authorities);
        }
        return null;
    }
}

认证失败处理

默认是跳转登录界面

@Component
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response,
                       AuthenticationException authException) throws IOException {
    response.setCharacterEncoding("utf-8");
    response.setContentType("text/javascript;charset=utf-8");
    response.getWriter().print("未登录,没有访问权限");

  }
}

没有权限处理

默认是响应403

public final class MyAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/javascript;charset=utf-8");
        response.getWriter().print("没有访问权限");
    }
}

JWT token处理工具类

这里对过期的token直接抛出异常,并且没做处理,请自行处理

public class JwtTokenUtil {
  // Token请求头
  public static final String TOKEN_HEADER = "Authorization";
  // Token前缀
  public static final String TOKEN_PREFIX = "Bearer ";
  // 过期时间
  public static final long EXPIRITION =  24 * 60 * 60 ;
  // 应用密钥
  public static final String APPSECRET_KEY = "123123";
  // 角色权限声明
  private static final String ROLE_CLAIMS = "role";
   
  /**
   * 生成Token
   */
  public static String createToken(String username,String role) {
    Map<String,Object> map = new HashMap<>();
    map.put(ROLE_CLAIMS, role);
 
    String token = Jwts
        .builder()
        .setSubject(username)
        .setClaims(map)
        .claim("username",username)
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION * 1000))
        .signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
    return token;
  }
 
  /**
   * 校验Token
   */
  public static Claims checkJWT(String token) {
    try {
      final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
      return claims;
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }
 
  /**
   * 从Token中获取username
   */
  public static String getUsername(String token){
    Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
    return claims.get("username").toString();
  }
 
  /**
   * 从Token中获取用户角色
   */
  public static String getUserRole(String token){
    Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
    return claims.get("role").toString();
  }
 
  /**
   * 校验Token是否过期
   */
  public static boolean isExpiration(String token){
    Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
    return claims.getExpiration().before(new Date());
  }
}

配置类

仅适用于新版本的security

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Autowired
    JWTAuthorizationFilter jwtAuthorizationFilter;

    /**
     * 认证管理器,登录的时候参数会传给 authenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
        // 设置部分接口权限
        http.authorizeHttpRequests(authorize -> authorize
                // 37是角色id
                .requestMatchers(antMatcher("/user/**")).hasRole("37")
                .requestMatchers(antMatcher("/user2/**")).hasRole("380")
                .requestMatchers(regexMatcher("/admin/.*")).hasRole("admin")
                .anyRequest().authenticated()
        );

        // 添加JWT登录拦截器
        http.addFilter(new JWTAuthenticationFilter(authenticationManager, true))
                // 添加JWT鉴权拦截器
                .addFilterBefore(jwtAuthorizationFilter, BasicAuthenticationFilter.class)
                // 设置Session的创建策略为:Spring Security不创建HttpSession
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // 认证异常(匿名用户访问无权限资源时的异常处理),// 没有访问权限异常处理器
        http.exceptionHandling((exceptions) -> exceptions
                .authenticationEntryPoint(new JWTAuthenticationEntryPoint())
                .accessDeniedHandler(new MyAccessDeniedHandler()));

        // 跨站请求伪造拦截关闭,前后端分离,并且使用jwt机制,这个并没有作用。
        http.csrf().disable();

        return http.build();
    }

}

跨域配置

如果前后端地址不一致,还需要开启跨域配置

其他实现方式

我们可以直接在配置中放通某个接口的权限,然后在接口中进行认证操作,由于AuthenticationManager已经被注入容器,所以我们是可以获取到的

@Autowired
private AuthenticationManager authenticationManager;

    @Autowired
    private AuthenticationManager authenticationManager;

@Override
public ResponseResult login(User user) {
    // AuthenticationManager的authenticate 进行用户认证
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
    Authentication authenticate = authenticationManager.authenticate(authenticationToken);
 ......   
}

新版本security配置生效规则

WebSecurityConfigurerAdapter被提示已经废弃,我们可以使用@Bean来构建相关配置,HttpSecurityConfiguration中有@Autowired来引入我们的配置,没有配置也会有默认的配置生效,当然还有一些其他的配置类

总之之前我就没弄明白这些个配置的逻辑,无语,照葫芦画瓢吧

控制一个用户只能登陆一次

用户在这个手机登录后,他又在另一个手机登录相同账户,对于之前登录的账户是否需要被挤兑,或者说在第二次登录时限制它登录,更或者像腾讯视频VIP账号一样,最多只能五个人同时登录,第六个人限制登录。

​ security默认有一个过滤器,我们进行配置后,ConcurrentSessionFilter会进行相关控制

​ 但是现在我们显然不能够再使用这套机制,JWT本身不会在后端存储,如果我们需要控制,那么可以根据 (用户id ) 存储一个(用户当前登录端)数据到redis中,然后就是客户在机器a登录后,存放redis信息,用户在b机器上登录后,更新redis信息,让a客户端再次请求时,响应前端你已经被登出之类的信息。

​ 这个模式同样可以支持多端登录,但是每个终端只能登陆一次,比如app端和pc端

动态权限

官方文档

Security新版本中AuthorizationFilter替代了FilterSecurityInterceptor进行权限验证,然后交给authorizationManager进行权限判断

,然后RequestMatcherDelegatingAuthorizationManager#check就会通过我们配置类中设置的权限配置器一个一个的进行匹配;相比原先FilterSecurityInterceptor中的验证个人感觉简单了许多,而且FilterSecurityInterceptor相关的AccessDecisionManager也已经废弃

首先就是你要确定你验证权限的模式。

1、比如你可以设置接口只要登录就能访问,这就没事了

2、登录基础上,都能访问,当时前端会查询权限列表,根据权限展示页面

3、在2的基础上,虽然前端验证了,但是后端还是需要验证

然后我们继续在第三步基础上往后讲

后端要验证权限,首先就是根据角色划分权限,然后就是角色之间是否互斥(这个应该是配置的时候控制),用户是否只能使用单个角色登录,或者一登录就是包含了其所有角色合并的权限,角色是否分级

相关概念

我们示例中默认是包含了所有角色权限的

这是官方的实例,我并没有测试,只是告诉你大致可以这么配置复杂的权限

这个示例似乎不太友好,至少如果权限很多的话,过滤器中for循环可能会耗费一段时间

    @Bean
    SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> access)
            throws Exception {
        http.authorizeHttpRequests((authorize) -> authorize
                        .anyRequest().access(access));
        // ...

        return http.build();
    }

    @Bean
    AuthorizationManager<RequestAuthorizationContext> requestMatcherAuthorizationManager(HandlerMappingIntrospector introspector) {
        MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
        RequestMatcher permitAll =
                new AndRequestMatcher(
                        mvcMatcherBuilder.pattern("/resources/**"),
                        mvcMatcherBuilder.pattern("/signup"),
                        mvcMatcherBuilder.pattern("/about"));
        RequestMatcher admin = mvcMatcherBuilder.pattern("/admin/**");
        RequestMatcher db = mvcMatcherBuilder.pattern("/db/**");
        RequestMatcher any = AnyRequestMatcher.INSTANCE;

        AuthorizationManager<HttpServletRequest> manager = RequestMatcherDelegatingAuthorizationManager.builder()
                .add(permitAll, (authentication, object) -> new AuthorizationDecision(true))
                .add(admin, AuthorityAuthorizationManager.hasRole("ADMIN"))
                .add(db, AuthorityAuthorizationManager.hasRole("DBA"))
                .add(any, new AuthenticatedAuthorizationManager())
                .build();

        return  (authentication, context) -> manager.check(authentication,context.getRequest());

    }

当然我们基础示例中讲过,这个access中可以自定义是否授权,所以我们可以匹配所有请求,然后走我们自定义的方法

这里是自定义AuthorizationManager,实现其check方法

httpSecurity.authorizeHttpRequests()
                .anyRequest()
                .access((authenticationSupplier, requestAuthorizationContext) -> {
                    // 当前用户的权限信息 比如角色
                    Collection<? extends GrantedAuthority> authorities = authenticationSupplier.get().getAuthorities();
                    // 当前请求上下文
                    // 我们可以获取携带的参数
                    Map<String, String> variables = requestAuthorizationContext.getVariables();
                    // 我们可以获取原始request对象
                    HttpServletRequest request = requestAuthorizationContext.getRequest();
                    //todo 根据这些信息 和业务写逻辑即可 最终决定是否授权 isGranted
                    boolean isGranted = true;
                    return new AuthorizationDecision(isGranted);
                });

也可以

.anyRequest().access("@mySecurityExpression.hasPermission(request,authentication)")

@Component
public class MySecurityExpression{
    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        // 获取主体
        Object obj = authentication.getPrincipal();
        if (obj instanceof UserDetails){
            UserDetails userDetails = (UserDetails) obj;
            //
            String name = request.getParameter("name");
            //获取权限
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            //判断name值是否在权限中
            return authorities.contains(new SimpleGrantedAuthority(name));
        }
        return false;
    }
}

总之这样之后,你大概可以缓存全部权限数据,然后每次登陆会从缓存中获取权限,和从token中获取的登录用户的权限,然后进行判定是否有权限登录

缓存的话是可以修改的,比如使用ConCurrentHashMap缓存,然后用角色作为key,角色的权限列表作为value,修改某个角色的权限时,甚至由于ConcurrentHashMap的是多线程=安全的,我们直接修改某个key,完全不会有任何影响。权限修改既生效,当然你还要修改后用户访问报错的问题(比如你可以通知前端再次获取一下权限,websocket连接的情况下,总之似乎包没有权限也没啥)。

其他登录方式

  • 微信认证
  • QQ认证
  • 手机号+验证码认证
  • 图形验证码认证
  • 邮箱认证

微信认证和qq认证不知道是不是可以直接通过oauth2配置实现,然后剩下的的三种模式完全可以套用我示例中的实现。

可以参考本文 其他实现方式,一种登录登录方式一个接口,登录成功后统一返回token.

功能点

比如实现修改密码后用户退出登录: redis缓存

用户在使用过程中token失效怎么处理: 可以添加一个refresh_token,前端设置一个刷新token的间隔时间,当用户请求时,判断一下是否需要刷新,如果用户长时间没有操作,导致token和refresh_token都失效了,那么就提示用户重新登录吧(具体看业务而定),一般refresh_token是需要进行存储的

app第一次登录成功后,每次打开都不需要再次输入登录密码:大致和security rememberme 功能的实现是一个概念,remember me 会有一个cookie设置失效时间,这样cookie关闭浏览器也不会失效,后端有专门的表对这样的信息进行存储, 然后app端的话应该也是需要存储一个这样的数据,然后到后端进行匹配,一般这样的数据应该和机器有一定关联性,总之换了台机器应该就没用了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/74225.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PCB入门学习—原理图的绘制1(MCU部分)

目录 2.1 STM32F103VET6 MCU核心电路的绘制 学习目录 2.1 STM32F103VET6 MCU核心电路的绘制 总结&#xff1a;放置元件&#xff0c;连线&#xff0c;放置网络标号&#xff0c;更新序号。 主控放上去之后原理图图纸不太够&#xff1a;双击右边边缘&#xff0c;默认图纸大小是…

【Java小案例】从简到精完美判断年份是闰/平年和该年二月份有几天

目录前言问题描述思路分析解决方案方案一方案二方案三方案四结语前言 1、平年指阳历没有闰日或农历没有闰月的年份&#xff0c;闰年是公历中的名词&#xff0c;是为了弥补因人为历法规定造成的年度天数与地球实际公转周期的时间差而设立的&#xff0c;补上时间差的年份为闰年&a…

QA | SWCF2022 笔记:GNSS模拟赋能汽车HIL测试

2022年度SWCF卫星通信与仿真测试研讨会正在进行中&#xff0c;精彩演讲&#xff1a;GNSS模拟赋能汽车HIL测试&#xff0c;感谢大家的观看与支持&#xff01;收到一些粉丝的技术问题&#xff0c;我们汇总了热点问题并请讲师详细解答&#xff0c;在此整理分享给大家&#xff01; …

高通平台开发系列讲解(UART篇)高速串口代码流程

文章目录 一、初始化1.1、Registration with the SPS driver1.2、UART port registration二、Port open三、Port close沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇章主要介绍高通平台高速串口代码流程。 一、初始化 初始化流程: msm_serial_hs_init() ->

数据传送指令MOV、XCHG

学习过程中要重点掌握对标志寄存器的影响 数据传送类指令&#xff08;不影响标志位&#xff09; 一&#xff1a;MOV指令 先要知道图片中这几个英文表示什么 立即数&#xff08;immediaate operand&#xff09; 寄存器&#xff08;register&#xff09; 内存&#xff08;…

WEB前端大作业HTML静态网页设计旅游景点区主题——三亚旅游网页设计

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

基于Java+Swing实现超级玛丽游戏

基于JavaSwing实现超级玛丽游戏一、系统介绍二、功能展示三、其他系统一、系统介绍 超级玛丽小游戏的JAVA程序&#xff0c;进入游戏后首先按空格键开始&#xff0c;利用方向键来控制的马里奥的移动&#xff0c;同时检测马里奥与场景中的障碍物和敌人的碰撞&#xff0c;并判断马…

JVM八股文,面试会被问到什么?都在这里啦 ~

目录 1、JVM内存划分 1.1、程序计数器&#xff08;Program Counter Register&#xff09; 1.2、方法区&#xff08;Method Area&#xff09; 1.3、本地方法栈&#xff08;Native Method Stacks&#xff09; 1.4、虚拟机栈&#xff08;JVM Stacks&#xff09; 1.5、Java堆…

溢出的文字省略号显示

溢出的文字省略号显示 1、单行文本溢出显示省略号 源代码 必须满足三个条件&#xff1a;white-space: nowrap; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; nowrap强制一行内显示文本(默认normal自动换行)&#xff0…

vscode跨语言调试

所谓“工欲善其事&#xff0c;必先利其器”&#xff0c;作为一个程序员&#xff0c;调试在项目开发过程中的重要性自然是不言而喻的。 最近项目中遇到的项目是由python和cpp完成的&#xff0c;python端会调用到cpp的库。由于做二次开发需要进行跨语言联调&#xff0c;所以在这…

QList与QVector遍历方法与性能比较

目录 一、 性能测试 二、 QList与QVector耗时对比分析 三、QList遍历方式对比分析 四、QVector遍历方式对比分析 一、 性能测试 最近使用opengl画点云数据时发现比较卡顿&#xff0c;原因是我使用了QList数据结构&#xff0c;后面改为QVector改善很多&#xff0c;速度提升1倍。…

什么是数学思维

什么是数学 数学 [英语&#xff1a;mathematics&#xff0c;源自古希腊语μάθημα&#xff08;mthēma&#xff09;&#xff1b;经常被缩写为math或maths]&#xff0c;是研究数量、结构、变化、空间以及信息等概念的一门学科。 数学 是人类对事物的抽象结构与模式进行严格…

大学生影视主题网页制作 HTML+CSS+JS仿360影视网站 dreamweaver电影HTML网站制作

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 文章目录一、网页介绍一…

数据结构:排序

目录 插入排序 插入排序 希尔排序 选择排序 选择排序 堆排序 交换排序 冒泡排序 快速排序 递归实现&#xff1a; ●hoare版本 ●三数取中小区间法优化 ●挖坑版本 ●双指针版本 非递归 ●用栈实现 ●用队列实现 归并排序 ● 递归 ●非递归 总结 来了朋友&a…

slot的理解

首先&#xff0c;大概了解一下插槽&#xff1a; 插槽是什么 ![在这里插入图片描述](https://img-blog.csdnimg.cn/90b04660769e49c286ee2e1821d2a2bb.png 插槽&#xff1a;在HTML中 slot 元素 &#xff0c;作为 Web Components 技术套件的一部分&#xff0c;是Web组件内的一个占…

HashMap1.8也会发生死循环

在网上搜资料时候然后发现网上都说1.7版本的HashMap会发生死链也就是死循环&#xff0c;但是在HashMap中也会产生死循环&#xff0c;接下来直接看代码吧 代码 类名字我忘记改了这是我以前看park时候弄的但是这不重要 当你运行 public class parkAndUnpark {static Map<…

微服务守护神-Sentinel-降级规则

引言 书接上篇 微服务守护神-Sentinel-流控规则 &#xff0c;上面介绍了Sentinel流控规则&#xff0c;本篇继续来Sentinel的降级规则。 降级规则 那啥为降级呢&#xff1f;降级可以理解为下降等次&#xff0c;比如&#xff1a;你从广州到北京&#xff0c;有钱时&#xff0c;…

Kafka的认证

Kafka支持基于SSL和基于SASL的安全认证机制。 基于SSL的认证主要是指Broker和客户端的双路认证。即客户端认证broker的证书&#xff0c;broker也认证客户端的证书。 Kafka还支持通过SASL做客户端认证。SASL是提供认证和数据安全服务的框架。Kafka支持的SASL机制有5种&#xff…

Docker容器化技术入门(一)Docker简介

Docker容器化技术入门&#xff08;一&#xff09;Docker简介前言&#xff08;一&#xff09;Docker简介1 Docker是什么&#xff1f;1.1 Docker的出现1.2 Docker的理念1.3 一句话2 容器与虚拟机比较2.1 容器发展简史2.2 传统虚拟机技术2.3 容器虚拟化技术2.4 对比3 Docker能干什…

华硕编程竞赛11月JAVA专场 D题飞机大战 题解

作者主页&#xff1a;Designer 小郑 作者简介&#xff1a;Java全栈软件工程师一枚&#xff0c;来自浙江宁波&#xff0c;负责开发管理公司OA项目&#xff0c;专注软件前后端开发&#xff08;Vue、SpringBoot和微信小程序&#xff09;、系统定制、远程技术指导。CSDN学院、蓝桥云…