微服务-14 安全框架SpringSecurity配合Jwt(登录,加密,验证,Redis存储,白名单,退出,权限,异常,CSRF)内容有点多

安全认证,权限校验框架SpringSecurity
 本文涉及到得源码 地址 (nacos-xxxx1.2 子项目集成了) https://gitee.com/langjunnan/nacos-parent-security
SpringSecurity简介: 这是Spring官网提供的安全认证框架,主要解决 登录,权限认证,安全防护攻击,校验token,校验表单等,提供了一些过滤器链
 
SpringSecurity入门:
 
    框架提供了15个过滤器 具体那个过滤器执行否,我们需要自己去配置,并且我们需要给每一个需要的过滤器添加 符合自身业务系统的认证代码  ,下面是所有过滤器链,每个过滤器都有不同的功能和用处
    1  省略 看上图
    2  省略 看上图
    3 CsrfFilter 外部csrf攻击过滤器
    4  省略 看上图
    5 UsernamePasswordAuthenticationFilter 过滤器 用户以 用户名 密码方式登录
    6 DefaultLoginPageGeneratingFilter 这是集成SpringSecurity默认提供的登录页面(非前后端分离)
    7 DefaultLogoutPageGeneratingFilter 这是集成SpringSecurity默认提供的退出登录页面(非前后端分离)
    8 省略 看上图
    9 省略 看上图
    10省略 看上图
    11省略 看上图
    12 SessionManagementFilter Session方式登录 验证处理
    13 ExceptionTranslationFilter (处理所有异常捕获 过滤器 处理用户出现的异常
    14 FilterSecurityInterceptor 登录成功后  验证当前登录人权限
 
认证授权流程(框架默认的流程)我在网上找的两张图哦 都是一个意思
1、用户浏览器登录页面 输入 用户名 和密码 信息,框架执行 UsernamePasswordAuthenticationFilter 过滤器 此时 用户名 密码已经封装成一个Authentication接口的用户名和密码实现类,此时Authentication对象还没有权限
2、调用AuthenticationManager接口的实现类ProviderManager
3、ProviderManager 委托AbstractUserDetailsAuthenticationProvider接口的实现类DaoAuthenticationProvider认证
4、DaoAuthenticationProvider 调用 UserDetailsService接口的实现类InMemoryUserDetailsManager进行认证
5、InMemoryUserDetailsManager 内部 会进行 根据用户名 查询用户是否存在,在内存当中查询用户信息权限信息
6、查询到的用户信息 和 权限信息 封装成 一个UserDetails接口对象返回给DaoAuthenticationProvider
7、DaoAuthenticationProvider 内部会通过PasswordEncoder验证 Authentication密码和 UserDetails 密码是否一致,
8、如果验证通过 UserDetails 中的用户信息 权限信息 也会封装到 Authentication 对象当中返回给 UsernamePasswordAuthenticationFilter,然后给存储到SecurityContextHolder.getContext().setAuthentication()方法 保存到上下文中
9、后续用户再次请求直接从上下文中获取
认证总结: 
    1、上面是SpringSecurity默认的执行流程,我们要按照自己的业务进行改造,例如 UserDetailsService 接口是用的  InMemoryUserDetailsManager 实现类 在内存当中获取  用户信息 我们自然要修改成关系型数据库中读取,我们可以重写UserDetailsService 接口 的实现
    2、我们也不用框架自己携带的UsernamePasswordAuthenticationFilter 我们自己写Controller 然后 同样也用ProviderManager 类调用
 
自定义登录,验证,集成 Jwt: 
登录
    自定义登录接口,调用 ProviderManager方法进行认证  重写UserDetailsService实现类,验证通过 生成jwt,把用户信息存入redis中
校验
    编写jwt过滤器, 获取token  解析token  去redis中获取用户信息
1准备工作:pom文件
    
<!--SpringSecurity-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
 
 
创建数据用户表
 
 
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
 
 
-- ----------------------------
-- Table structure for userInfo
-- ----------------------------
DROP TABLE IF EXISTS `userInfo`;
CREATE TABLE `userInfo`  (
  `userId` int(11) NOT NULL COMMENT '用户id',
  `userName` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '用户名',
  `phone` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '手机号',
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '密码',
  PRIMARY KEY (`userId`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
 
 
SET FOREIGN_KEY_CHECKS = 1;
 
 
1.1开始编写: 从数据库中读取用户信息认证返回 重写UserDetailsService 接口的实现类
package com.nacos.service.impl;
 
 
import com.nacos.bo.LoginUser;
import com.nacos.bo.UserInfo;
import com.nacos.mapper.UserInfoMapper;
import org.springframework.security.core.GrantedAuthority;
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 javax.annotation.Resource;
import java.util.Collection;
import java.util.Objects;
 
 
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
 
 
    @Resource
    UserInfoMapper userInfoMapper;
 
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //我们重写了 UserDetailsService 类下的 认证信息。  我们不在缓存中查询用户。 而是去数据库查询用户
 
 
        UserInfo info= userInfoMapper.queryUserInfo(username);
        if(Objects.isNull(info)){
            throw  new RuntimeException("用户 不存在");
        }
 
 
        //1 UserDetails 是接口 所以我们需要编写实现类 LoginUser,
        //2 最终的目的是 返回当前登录对象给 UserDetails  而 UserDetails 实现类LoginUser 并没有具体用户信息
        // 所以我们把 查询到的UserInfo 赋值给 实现类LoginUser 返回
        LoginUser loginUser=new LoginUser();
        loginUser.setUserInfo(info);
 
 
        return loginUser;
 
 
    }
}
copy出来的代码
copy出来的代码
package com.nacos.bo;
 
 
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
 
 
import java.util.Collection;
 
 
// 框架需要的用户对象
public class LoginUser implements UserDetails {
 
 
    //本项目内部的用户对象
    private UserInfo userInfo;
 
 
    public UserInfo getUserInfo() {
        return userInfo;
    }
 
 
    public void setUserInfo(UserInfo userInfo) {
        this.userInfo = userInfo;
    }
 
 
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
 
 
    @Override
    public String getPassword() {
        return userInfo.getPassword();
    }
 
 
    @Override
    public String getUsername() {
        return userInfo.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;
    }
}
 
1.2启动项目 先简单初步测试一下
        这里我们数据库里面的用户名是123456  密码123,可以看到password 里面 前面多了一个 {noop123} 这句的意思是 告诉框架 我们库里存储的是明文密
 
码。框架默认是 对比加密后的密文 ,所以这里需要注意  SpringSecurity对密码的存储有这种格式的要求 例如存储 = {加密方式} 密码   后续改善了就没有了
 
打开浏览器  输入要访问的接口 127.0.0.1:8080/getTestRedis?value=456456  随后默认弹出 SpringSecurity的登录页面
然后我们输入库里存储的 123456 和 123 登录成功后回到 要访问的接口 
至此 第一个小测试案例结束  
 
2密码加密存储
    2.1 上面演示的案例 数据库的密码是铭文存储的 真正的项目当中我们不能用铭文 因为容易泄露 ,所以接下来 我们给密码加密操作,Security给我们提供了BCryptPasswordEncoder 对象 ,BCryptPasswordEncoder 对象提供了加密方法,和 明文比对加密 方法  
    2.2 使用BCryptPasswordEncoder再次测试
1 把刚刚测试的密文 替换到数据库中的密码字段里
2 在程序中增加 BCryptPasswordEncoder 类,其余的而验证对比操作, 框架已经给我们做好了
copy出来的代码
package com.nacos.security.config;
 
 
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.stereotype.Component;
 
 
 
 
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
 
 
    /**
     * security5.0以上版本 默认必须 密码 加密
     * 密码加密生成器
     *
     * @return PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
 
 
        return new BCryptPasswordEncoder();
    }
 
 
}
2.3测试  和上面访问网页 步骤一样  账号密码也一样,是可以访问的。 
 
3引入jwt(json web token)为我们生产 加密的(撒盐)token 并且还携带 验证token 
 
3.1 引入jar包
<!-- jwt-->
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.0</version>
</dependency>
 
添加jwt工具类,如下图
 
 
jwt类如下:
 
package com.nacos.security.common;
 
 
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
 
 
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
 
 
@Component
public class JwtUtil {
    Logger log= LoggerFactory.getLogger(JwtUtil.class);
 
 
    @Value("${token.expireTime}")
    private Long expireTime;
 
 
    @Value("${token.secret}")
    private String secret;
 
 
    /**
     * 构建 jwt token串
     *
     * @param jwtContent
     * @return String
     */
    public String generateToken(JwtContent jwtContent) {
 
 
        Date nowDate = new Date();
        Date expireDate = new Date(System.currentTimeMillis() + expireTime * 1000L);
        /*
         * jwt的头部承载两部分信息:
         * 声明类型,这里是jwt
         * 声明加密的算法 通常直接使用 HMAC SHA256
         * {
         *   'typ': 'JWT',
         *   'alg': 'HS256'
         * }
         *
         * playload
         * 载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
         * 标准中注册的声明
         * 公共的声明
         * 私有的声明
         *
         * signature
         * jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
         * header (base64后的)
         * payload (base64后的)
         * secret 私钥
         */
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(JSON.toJSONString(jwtContent))
                .setIssuedAt(nowDate)   //设置生成 token 的时间
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
 
 
    /**
     * 获取凭证信息
     *
     * @param token jwt token串
     * @return Claims
     */
    public Claims getClaimByToken(String token) {
        try {
            if (StringUtils.startsWithIgnoreCase(token, "Bearer ")) {
                token = token.split(" ")[1];
            }
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            log.error("[getClaimByToken]:token 凭证信息有误! {}", e.getMessage());
            HttpServletRequest request =((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String authorization = request.getHeader("Authorization");
            String url = request.getRequestURL().toString();
            String uri = request.getRequestURI();
            log.error("authorization==>" + authorization + ", url==>" + url + ", uri==>" + uri);
            return null;
        }
    }
 
 
    /**
     * 获取过期时间
     *
     * @param token jwt token 串
     * @return Date
     */
    public Date getExpiration(String token) {
        return getClaimByToken(token).getExpiration();
    }
 
 
    /**
     * 验证token是否失效
     *
     * @param token token
     * @return true:过期   false:没过期
     */
    public boolean isExpired(String token) {
        try {
            final Date expiration = getExpiration(token);
            return expiration.before(new Date());
        } catch (Exception e) {
            log.error("[JwtUtils --> isExpired]: {}", e.getMessage());
            return true;
        }
    }
 
 
    /**
     * 检验是否为 jwt 格式的字符串
     *
     * 说明: jwt 字符串由三部分组成, 分别用 . 分隔开, 所以认为有两个 . 的字符串就是jwt形式的字符串
     * @param token jwt token串
     * @return boolean
     */
    public boolean isJwtStr(String token){
        return StringUtils.countOccurrencesOf(token, ".") == 2;
    }
 
 
    /**
     * 获取 jwt 中的账户名
     *
     * @param token jwt token 串
     * @return String
     */
    public String getAccountName(String token){
        String subject = getClaimByToken(token).getSubject();
        JwtContent jwtContent = JSONObject.parseObject(subject, JwtContent.class);
        jwtContent.getUserName();
        return jwtContent.getUserName();
    }
 
 
    /**
     * 获取 jwt 的账户对象
     * @param token
     * @return
     */
    public JwtContent getTokenSubjectObject(String token){
        Claims claimByToken = getClaimByToken(token);
        String subject = claimByToken.getSubject();
        String body = JSONObject.toJSONString(subject);
        Object parse = JSON.parse(body);
        String s = parse.toString();
        return JSONObject.parseObject(s,JwtContent.class);
    }
 
 
    /**
     * 获取 jwt 账户信息的json字符串
     * @param token
     * @return
     */
    public  String getTokenSubjectStr(String token){
        String body = JSONObject.toJSONString(getClaimByToken(token).getSubject());
        Object parse = JSON.parse(body);
        return parse.toString();
    }
}
jwt配置类里面引用了几个 变量  配置信息如下:
token:
  head: Authorization
  # token 有效期 1 天,单位秒
  expireTime: 86400
  secret: ^_^.[langjunnan].^_^
 
JwtContent类 如下
 
package com.nacos.security.common;
 
 
public class JwtContent {
 
 
    private String userName;
 
 
    private String phone;
 
 
    private int userId;
 
 
    public String getUserName() {
        return userName;
    }
 
 
    public void setUserName(String userName) {
        this.userName = userName;
    }
 
 
    public String getPhone() {
        return phone;
    }
 
 
    public void setPhone(String phone) {
        this.phone = phone;
    }
 
 
    public int getUserId() {
        return userId;
    }
 
 
    public void setUserId(int userId) {
        this.userId = userId;
    }
}
 
简单测试以下jwt工具类
public static void main(String[] args) {
    JwtUtil jwtUtil=new JwtUtil();
 
 
    jwtUtil.expireTime=10L;//10秒过期
    jwtUtil.secret="godNan";// 随便字符串  加密盐
    JwtContent content=new JwtContent();//自定义的实体类 用户信息 存储到jwt里
    content.setPhone("13189031999");
    content.setUserId(1);
    content.setUserName("张三");
    String token=jwtUtil.generateToken(content);
    System.out.println("生成的token是"+token);
 
 
    boolean flag= jwtUtil.isExpired(token);//token 是否有效?
    System.out.println("token是否过期"+flag);
 
 
    if(!flag){
        //如果没有过期那我们获取里面的信息
           JwtContent content1= jwtUtil.getTokenSubjectObject(token);
        System.out.println(content1.toString());
    }
    try {
        Thread.sleep(15000);//休眠15s  token一定国企 因为  token有效期 是10 s
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
     flag= jwtUtil.isExpired(token);//token 是否有效?
    System.out.println("token是否过期"+flag);
}
 
看结果:
 
4编写登录接口
    用户登录验证成功后,生成jwt(Token) 并且把当前用户信息存入Redis缓存中,然后把token返回给前端
    还记得SpringSecurity那个执行过程图吗, 我们要重写Controller ,就得想办法让 AuthenticationManager接口实现类调用到
4.1 暴露 SpringSecurity框架得AuthenticationManager实现类 我们还是在设置密码那个类里面 重写authenticationManagerBean方然后 在方法上增加一个@Bean 注解 SpringBoot容器就会托管了
4.2 为了给我们得项目增加安全性 我们接着重写configure 方法,让SpringSecurity来管理外部访问权限。除了登录接口其余接口未登录都不准访问
copy SpringSecurityConfig 类重写得两个 方法 
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}
 
 
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 前后端分离 项目 关闭csrf
    http.csrf().disable()
            //前后端分离 不通过SessionSecurity获取SecurityContext
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and().authorizeRequests()
                    //放开登录接口  未登录也可以访问
            .antMatchers("/login").anonymous()
                    .anyRequest().authenticated();//除了上面得请求 全部需要权限验证
 
 
}
4.3登录得Controller
copy代码
@RestController
public class LoginController {
 
    @Resource
    LoginService loginService;
 
    @PostMapping("/login")
    public PublicDTO login(@RequestBody RequestUser user){
 
    return  loginService.login(user);
 
    }
 
}
4.4登录得业务Service
copy代码
 
 
import com.nacos.bo.LoginUser;
import com.nacos.dto.PublicDTO;
import com.nacos.redis.RedisUtil;
import com.nacos.security.common.JwtContent;
import com.nacos.security.common.JwtUtil;
import com.nacos.service.LoginService;
import com.nacos.vo.request.RequestUser;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
 
 
import javax.annotation.Resource;
import java.util.Objects;
 
 
@Service
public class LoginServiceImpl implements LoginService {
    @Resource
    AuthenticationManager authenticationManager; //Security 提供得认证接口 ,刚刚暴露出来那个Bean
    @Resource
    JwtUtil jwtUtil; //token 工具类
    @Resource
    RedisUtil redisUtil;// 缓存
    @Override
    public PublicDTO login(RequestUser request) {
        //1 使用 AuthenticationManager 进行认证
        UsernamePasswordAuthenticationToken authenticationToken=new
                UsernamePasswordAuthenticationToken(request.userName,request.password);
 
 
        Authentication result= authenticationManager.authenticate(authenticationToken);
        //如果为空 那么验证失败
        if(Objects.isNull(result)){
            //2 如果认证没有通过给出提示
            throw  new RuntimeException("认证失败");
        }
        //3 如果认证通过了 使用userid 生成一个jtw 返回给前端
      LoginUser loginUser=(LoginUser) result.getPrincipal();// SpringSecurity得登录用户类
        JwtContent content=new JwtContent();
        content.setUserName(loginUser.getUsername());
        content.setUserId(loginUser.getUserInfo().getUserId());
        content.setPhone(loginUser.getUserInfo().getPhone());
        String token= jwtUtil.generateToken(content);
 
 
        //4 把完整的用户信息存入redis   userid 作为key
 
 
        String userId=loginUser.getUserInfo().getUserId()+"";
        redisUtil.set(userId,loginUser.getUserInfo());
        return PublicDTO.success(token,"登录成功");
    }
}
4.4 测试 登录  如下图验证成功
4.5 测试 登录  之Redis缓存是否有数据
 
5编写Jwt验证过滤器
5.1验证前端请求是否携带有效token)如果token有效通知Security为认证成功状态
package com.nacos.filter;
 
 
import com.nacos.bo.UserInfo;
import com.nacos.redis.RedisUtil;
import com.nacos.security.common.JwtContent;
import com.nacos.security.common.JwtUtil;
import io.seata.common.util.StringUtils;
import jodd.util.StringUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.List;
 
 
/**
* token 认证
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
 
 
    @Value("${token.head}")
    String authorization;
    @Resource
    JwtUtil jwtUtil;
    @Resource
    RedisUtil redisUtil;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
 
        //获取token                   authorization  配置文件中得 也是  Authorization  这里看着清晰  不用配置文件 配置得请求头
        String token= request.getHeader("Authorization");
        if(null ==token||"".equals(token)){
            //放行   还没有登录
            filterChain.doFilter(request,response);
            return;
        }
        //解析token
       boolean flag=jwtUtil.isJwtStr(token);
        if(!flag){
            throw new RemoteException("token不合法");
        }
        flag= jwtUtil.isExpired(token);
        if(flag){
            throw new RemoteException("token已经过期");
        }
        JwtContent content= jwtUtil.getTokenSubjectObject(token);
 
 
        //redis获取用户
        UserInfo userInfo= (UserInfo) redisUtil.get(content.getUserId()+"");
 
 
        if(null==userInfo){
            throw new RemoteException("用户未登录");
        }
 
         //用三个构造参数得  告诉SpringSecurity 该用户已经认证成功   里面有装药
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=
                    //       参数3权限集合
                    new UsernamePasswordAuthenticationToken(userInfo,null,null);
        //存入SecurityContext 中  因为 SpringSecurity 后面需要内部验证
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        filterChain.doFilter(request,response);
 
    }
}
注意这里
5.2 编写完了过滤器,需要把压着你哼token得过滤器交给SpringSecurity来管理,并且要放在UsernamePasswordAuthenticationFilter之前执行,我们还是在SpringSecurityConfig 类编写
5.3 验证: 
    先看一下未携带token访问是什么效果  返回403 服务器拒绝访问 ,意思就是没有权限
 
携带token 后得访问。 成功返回了
 
 
 
6退出登录
    6.1我们只需要把Redis缓存中得信息删除掉,因为在token验证过滤器中,有验证redis中是否存在数据,没有数据就是没有登录
    
copy代码
@Override
public PublicDTO loginOut() {
    //1可以从 上下文获取 userid   2也可以从jwt里面获取userid  只需要token
    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=(UsernamePasswordAuthenticationToken)
            SecurityContextHolder.getContext().getAuthentication();
    UserInfo userInfo=(UserInfo) usernamePasswordAuthenticationToken.getPrincipal();
    String userId=userInfo.getUserId()+"";
    redisUtil.remove(userId);
 
 
    return PublicDTO.success(userInfo.getUserName(),"退出成功");
}
6.2测试退出登录 在用token 去访问其他接口
结果 退出之后 token 不能访问系统了
 
 

 

7配置允许访问得静态资源(例如公司内部调用得白名单)只需要增加一行代码就OK

 

 

8权限配置

    8.1 SpringSecurity 给我们提供得 UsernamePasswordAuthenticationToken 类 里面就可以添加当前用户得权限集合,(但是我们不会取用它的 )先介绍一下框架内得权限
我们至少有两个地方需要添加权限,
 
    第一处   用户登录成功后 应该给LoginUser 赋予权限
   
    第二处
    过滤器验证token 后应该赋予权限
     8.2   我们考虑一下如果把当前用户得权限交给SpringSecurity来管理放在SpringSecurity上下文中,合适吗?,现在所有系统得权限都是持久化在数据库中得,如果我修改了数据库中得某个人得权限 那么每个服务节点中得SpringSecurity如何感知得到呢?
所以我们需要自己定义权限 RBAC模型,自己编写认证权限,读取数据库权限,我自己得社区用得权限模型如下图:
 
 
 
你也可以搞一个简单得RBAC 5张表, 这里涉及到内容太多了 后续单独拿出一篇文章在讲
 
9 自定义异常处理
    自定义 认证异常处理器 和权限不足处理器
copy配置类代码
@Resource //认证失败处理器
AuthenticationEntryPoint authenticationEntryPoint;
 
 
@Resource //授权失败处理器
AccessDeniedHandler accessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置异常处理器  认证异常处理器   内置得权限不足处理器
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);
}
copy认证异常处理代码
/**
* 认证失败
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
    //处理异常
        responseWriter(httpServletResponse, PublicDTO.success("用户认证失败 请重新登录"));
 
 
    }
 
 
    public void responseWriter( HttpServletResponse httpServletResponse,Object message) throws IOException {
 
 
 
 
        httpServletResponse.setStatus(200);
        httpServletResponse.setContentType("application/json");
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.getWriter().println(message);
    }
}
copy权限异常处理代码
/**
* 权限不足
*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
 
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        //处理异常
        responseWriter(httpServletResponse, PublicDTO.success("您得权限不足"));
    }
    public void responseWriter( HttpServletResponse httpServletResponse,Object message) throws IOException {
 
        httpServletResponse.setStatus(403);
        httpServletResponse.setContentType("application/json");
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.getWriter().println(message);
    }
}
 
10解决SpringSecurity跨域问题

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 1允许任何域名使用
        corsConfiguration.addAllowedHeader("*"); // 2允许任何头
        corsConfiguration.addAllowedMethod("*"); // 3允许任何方法(post、get等)
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig()); // 4
        return new CorsFilter(source);
    }
}
 
11 CSRF 跨域伪造攻击
你在当前A网站登录成功后 token Session 信息都放在浏览器得Cookie里了,这时候你在当前浏览器访问到黑客得B网站,
它伪造了一些表单。或者是一个图片。请求到你得A网站上了,而且Cookie信息 也都携带过去了 A网站以为是合法请求,其实B 携带了病毒,或者其他敏感信息
我们要解决这种问题,我们本次使用得token 前端不存储到Cookie  所以是可以防范得,
 
 
11 扩展  SpringSecurity 还提供了很多处理器 例如登录成功处理器,注销成功处理器,等等,不过我们使用得是自定义得Controller,配合Jwt(JsonWebToken) 验证,所以不要 那些处理器了
 
 
posted @ 2022-03-08 17:18  郎小乐  阅读(770)  评论(0)    收藏  举报