spring security鉴权

1.SpringSecurity 鉴权 - [重点]

  • RBAC 基于角色访问控制

Role-Based Access Control

组成部分:

RBAC模型里面,有3个基础组成部分,分别是:用户user、角色role 和 权限permssion
User(用户):每个用户都有唯一的UID识别,并被授予不同的角色
Role(角色):不同角色具有不同的权限
Permission(权限):访问权限
用户-角色映射:用户和角色之间的映射关系
角色-权限映射:角色和权限之间的映射



-- 基于资源(权限)  细粒  用户-权限
SELECT p.* FROM rbac_perms p INNER JOIN rbac_user_perm up ON p.id=up.permid
                             INNER JOIN rbac_user u ON u.id=up.userid
                             WHERE u.username='zhangsan'

-- 基于角色   粗粒度  用户-角色-权限

SELECT p.* FROM rbac_perms p INNER JOIN rbac_role_perm rp ON p.id=rp.permid
                           INNER JOIN rbac_user_role ur ON rp.roleid=ur.roleid
                           INNER JOIN rbac_user u ON  u.id=ur.userid
                           WHERE u.username='zhangsan'
  • 配置类注解

    step1:在配置类上添加@EnableWebSecurity

    step2:配置类上添加@EnableGlobalMethodSecurity指定scecurity鉴权时使用的是哪一套注解

    Spring Security 支持三套注解:

    jsr250 注解@DenyAll、@PermitAll、@RolesAllowed
    secured 注解@Secured
    prePost 注解@PreAuthorize、@PostAuthorize

实现步骤:

(1) 在配置类上添加注解配置

@EnableWebSecurity
//@EnableGlobalMethodSecurity(jsr250Enabled = true)  //开启Security注解鉴权
//@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)  //sprintSecurity自带 可以支持Spring EL表达式
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
}

(2) 在控制器方法上使用注解,表示必须拥有该注解标识的权限才能访问

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/userList")
    //一旦使用此注解,表示请求该方法的用户权限集里必须该权限标识符
    //@RolesAllowed("ROLE_teacher:list")  //访问到数据库表中的权限标识符必须以ROLE_开头,注解上的ROLE_可以省略
    //@Secured("ROLE_teacher:list") //访问到数据库表中的权限标识符必须以ROLE_开头 注解上的ROLE_不能省略
    //@PreAuthorize("hasAnyAuthority('teacher:list')") //使用hashAnyAuthority EL表达式,可以指定权限标识,不要求使用ROL_ 开头
    //@PreAuthorize("hasAnyRole('ROLE_teacher:list')") //使用hashAnyROLE EL表达式,可以指定权限标识,要求使用ROL_ 开头 ,数据库表中的权限标识也必须以ROLE开头
    @PreAuthorize("hasRole('ROLE_teacher:list')")
    public List<User> queryUserList(){
        return userService.list(null);
    }
}
  • 权限不足的处理方案

/**
 * 权限不足的处理
 */
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        ResponseResult<Void> result = ResponseResult.error(ResultCode.NO_PERMISSION);
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out= response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
        out.flush();
        out.close();
    }
}

配置类

http.authorizeRequests().antMatchers("/login", "/login.html")
    .permitAll().anyRequest().authenticated()
    .and().
    // 设置登陆页、登录表单form中action的地址,也就是处理认证请求的路径
    formLogin().loginPage("/login.html").loginProcessingUrl("/login")
    //登录表单form中密码输入框input的name名,不修改的话默认是password
    .usernameParameter("username").passwordParameter("password")
    //登录认证成功后默认转跳的路径
    //.defaultSuccessUrl("/home")
    // 前后端分离认证成功的处理器 -输出json
    .successHandler(myAuthenticationSuccessHandler)
    // 前后端分离认证失败的处理器 -输出json
    .failureHandler(myAuthenticationFailureHandler)
    .and()
    // 前后端分离处理未登录请求
    .exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint)
    // 前后端分离处理权限不足的请求
    .accessDeniedHandler(myAccessDeniedHandler);
//.failureUrl("/error1").permitAll();
//关闭CSRF跨域攻击防御
http.csrf().disable();

2.SpringSecurity整合JWT - [重点]

2-1 JWT概述

  • 有状态与无状态比较

有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份
有状态缺点是什么?
    - 服务端保存大量数据,增加服务端压力
    - 服务端保存用户状态,无法进行水平扩展
    - 客户端请求依赖服务端,多次请求必须访问同一台服务器

服务器不需要记录客户端的状态信息:
无状态服务器优点:
    - 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
    - 服务端的集群和状态对客户端透明
    - 服务端可以任意的迁移和伸缩
    - 减小服务端存储压力
  • 无状态登录流程

(1) 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
(2) 认证通过,将用户信息进行加密形成token字符串 [自我描述信息]
(3) 将生成token串发送到客户端
(4) 以后每次请求,客户端都携带认证的token 
(5) 服务的对token进行解密,判断是否有效。

image-20220706143701069

  • JWT的Token串生成

JWT全称是Json Web Token   是JSON风格轻量级的授权和身份认证规范

JWT的Token串由三部分组成

  • header    头信息 -采用base64编码生成--类型与生成时间

  • Payload     载荷 - 用户身份信息,过期时间,签发人  -采用base64编码

  • Signature   签名 是整个数据的认证信息- header+Payload+密钥 secret-RSA非对称加密技术生成

使用JWT实现服务端交互流程:

- 1、用户登录
- 2、服务的认证,通过后根据secret生成token
- 3、将生成的token返回给浏览器
- 4、用户每次请求携带token --header 通过客户端的请求头发送token串
- 5、服务端利用公钥解读jwt签名,判断签名有效后,从Payload中获取用户信息
- 6、处理请求,返回响应结果

2-2 实现生成token

  • 添加依赖jar

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

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.8.1</version>
</dependency>
  • 编写工具类JwtTokenUtil并测试

public class JwtTokenUtil {
    /**
     * 过期时间50分钟
     */
    private static final long EXPIRE_TIME = 5 * 60 * 10000;
    /**
     * jwt 密钥
     */
    private static final String SECRET = "woniuxy";

    /*
       生成签名  50分钟过期
     */
    public static String createSign(String userName) throws Exception {
        try {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            return JWT.create()
                    // 将 user id 保存到 token 里面
                    .withAudience(userName)
                    // 50分钟后token过期
                    .withExpiresAt(date)
                    //.withClaim()
                    //.withSubject(userName)
                    // token 的密钥
                    .sign(algorithm);
        }catch(Exception ex){
            ex.printStackTrace();
            throw new Exception("签名错误");
        }
    }

    /**
     * 根据token获取username
     * @param token
     * @return
     */
    public static String getUserId(String token) {
        try {
            String userId = JWT.decode(token).getAudience().get(0);
            return userId;
        } catch (JWTDecodeException e) {
            throw new JWTDecodeException("生成的token 异常");
        }
    }

    /**
     * 校验token 是否有效
     * @param token
     * @return
     */
    public static boolean checkSign(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    // .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (JWTVerificationException exception) {
            throw new RuntimeException("token 无效,请重新获取");
        }
    }

    public static void main(String[] args) throws Exception {
        //测试生成Token串
        String strToken = JwtTokenUtil.createSign("zhangsan");
        System.out.println(strToken);

        //eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo
        //验证 token是否有效
        boolean isValid = JwtTokenUtil.checkSign("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo");
        System.out.println(isValid);

        //从给定的token串获取用户信息
        String username = JwtTokenUtil.getUserId("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ6aGFuZ3NhbiIsImV4cCI6MTY1NzA5MzQ3MX0.OdoENj363dPW2YVQfrc4SigoYlt45ydEtkgIc4xzzRo");
        System.out.println(username);
    }
}

2-3 SpringSecurity整合JWT-返回JWT token

image-20220706154521948

第1步,登录认证成功,生成token并返回

**
 * 自定义认证成功的处理器Handler
 */
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        try {
            //获取当前登录认证成功的用户名
            String username = request.getParameter("username");
            String strToken = JwtTokenUtil.createSign(username);

            //通过响应的json返回客户端
            ResponseResult<String> result = new ResponseResult<>(strToken,"OK",200);
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            //将对象转json输出
            out.write(new ObjectMapper().writeValueAsString(result)); 
            out.flush();
            out.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

第2步,携带Token发送请求

要想使FilterSecurityInterceptor过滤器放行:

1. Spring Security 上下文( Context ) 中要有一个 Authentication Token ,且应该是已认证状态。
2. Authentication Token 中所包含的 User 的权限信息要满足访问当前 URI 的权限要求。

实现思路:

关键在于:在 FilterSecurityInterceptor 之前 要有一个 Filter 将用户请求中携带的 JWT 转化为 Authentication Token 存在 Spring Security 上下文( Context )中给 “后面” 的 FilterSecurityInterceptor 用。

基于上述思路,我们要自定义实现一个 Filter :

/**
 *  将用户请求中携带的 JWT 转化为 Authentication Token
 *  存入 Spring Security 上下文( Context )
 *  表示每次请求只执行该过滤器一次
 */
@Component
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        //获取当前请求的uri
        String uri = request.getRequestURI();
        //如果是认证请求
        if(uri.endsWith("/login")){
            //放行
            filterChain.doFilter(request,response);
            return;
        }

         //不是认证请求--获取请求中的头部的token串
        String strToken = request.getHeader("strToken");
        if(StringUtils.isEmpty(strToken)){
            //抛出自定义异常 -Token为null
            myAuthenticationFailureHandler.onAuthenticationFailure(request,response,
                    new TokenIsNullException("Token为空!"));
            return ;
        }
        //不是空,且不是认证请求
        try {
            //检验token是否有效
            if (JwtTokenUtil.checkSign(strToken)) {
                //获取token中的用户名
                String username = JwtTokenUtil.getUserId(strToken);
                //查询数据库获取用户的权限集
                List<String> percodes = userMapper.getPerCodesByPerm(username);
                List<GrantedAuthority> authorities = new ArrayList<>();
                percodes.forEach(percode->{
                    authorities.add(new SimpleGrantedAuthority(percode));
                });
                //封装数据库存询的用户信息
                UsernamePasswordAuthenticationToken  usernamePasswordAuthenticationToken
                        = new UsernamePasswordAuthenticationToken(username,"",authorities);

                //存入securityContext
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                //放行
                filterChain.doFilter(request,response);
            }
        }catch(Exception e){
            ///抛出自定义异常  Token无效
            myAuthenticationFailureHandler.onAuthenticationFailure(request,response,
                    new TokenIsInvalidException("Token无效!"));
        }
    }
}

然后将过滤器插入到FilterChainPrxoy代理的过滤器链中的UsernamePasswordAuthencationFilter前面

//将自定义的JwtTokenAuthenticationFilter插入到过滤器链中的指定的过滤器前面
             http.addFilterBefore(jwtTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

3.注销成功处理方案

注销成功之后返回登录的页面,逻辑是没有错的,但是在前后端分离的情况下是返回登录页面吗?显然不是,而是返回注销成功的信息

于是,我们再去定制一个LogoutSuccessHandler

//注销成功的处理器
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                Authentication authentication) throws IOException, ServletException {
        String headerToken = request.getHeader("strToken");
        System.out.println("logout header Token:"+headerToken);
        if(!StringUtils.isEmpty(headerToken)){ //如果token不是空
            SecurityContextHolder.clearContext(); //清空上下文 用户名与权限集UsernamePasswordAuthenticationToken
            ResponseResult<String> result = new ResponseResult<>("","注销成功",200);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(new ObjectMapper().writeValueAsString(result));
        }else{
            ResponseResult<Void> result = ResponseResult.error(ResultCode.TOKEN_IS_NULL);
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out= response.getWriter();
            out.write(new ObjectMapper().writeValueAsString(result)); //将对象转json输出
            out.flush();
            out.close();
        }
    }
}

WebSecurityConfig配置类中 配置注销成功处理器

 // 前后端分离处理注销成功操作
 .and().logout().logoutSuccessHandler(myLogoutSuccessHandler);

//关闭session最严格的策略 -JWT认证的情况下,不需要security会话参与
 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

  • 4
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值