Spring-Security前后端分离权限认证

前后端分离

一般来说,我们用SpringSecurity默认的话是前后端整在一起的,比如thymeleaf或者Freemarker,SpringSecurity还自带login登录页,还让你配置登出页,错误页。

但是现在前后端分离才是正道,前后端分离的话,那就需要将返回的页面换成Json格式交给前端处理了

SpringSecurity默认的是采用Session来判断请求的用户是否登录的,但是不方便分布式的扩展,虽然SpringSecurity也支持采用SpringSession来管理分布式下的用户状态,不过现在分布式的还是无状态的Jwt比较主流。 所以怎么让SpringSecurity变成前后端分离,可以采用Jwt来做认证

什么是jwt

Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC7519).该token被设计为紧凑且==安全==的,特别适用于==分布式站点的单点登录(SSO)场景==。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

官网: JSON Web Token Introduction - jwt.io

jwt的结构

. 分割   三部分 

Header

Header 部分是一个JSON对象,描述JWT的元数据,通常是下面的样子。

{

"alg": "HS256",

"typ": "JWT"

}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256 (写成 HS256) ;typ属性表示这个令牌(token)的类型(type), JWT令牌统一写为JWT。

最后,将上面的JSON对象使用Base64URL算法转成字符串。

Payload(载荷)

Payload 部分也是一个JSON对象,==用来存放实际需要传递的数据==。JWT规定了7个官方字段,供选用。

iss (issuer):签发人

exp (expiration time):过期时间

sub (subject):主题

aud (audience):受众

nbf (Not Before):生效时间

iat (lssued At):签发时间

jti (JWT ID):编号

除了官方字段,==你还可以在这个部分定义私有字段==,下面就是一个例子。

{

"sub": "1234567890",

"name" : "John Doe",

“userid”:2

"admin": true

}

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把==秘密信息==放在这个部分。这个JSON 对象也要使用Base64URL 算法转成字符串。

Signature

Signature部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个==密钥(secret)==。这个密钥只有==服务器才知道==,不能泄露给用户。然后,使用Header里面指定的==签名算法(默认是 HMAC SHA256)==,按照下面的公式产生签名。

HMACSHA256(

base64UrlEncode(header) + ".”"+base64UrlEncode(payload),

secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

1.项目添加hutool依赖

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.18</version>
        </dependency>

http://t.csdnimg.cn/TA0Xx基于文章中连接数据库的实例基础上进行的前后端分离设计

2.搭建好一个vue项目

所需的导入包

3.修改配置文件 main.js

全局导入引入

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false


import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';


Vue.use(ElementUI);
import axios from 'axios'
// 后端项目的时候  http://localhost:8080
// axios设置一个默认的路径
// 创建实例时配置默认值
const instance = axios.create({
  // 访问路径的时候假的一个基础的路径
  baseURL: 'http://localhost:8080/',
  // withCredentials: true
});

请求拦截器与响应拦截器

// 请求拦截器
//
instance.interceptors.request.use( config=> {
  // config 前端  访问后端的时候  参数
  // 如果sessionStorage里面于token   携带着token  过去
  if(sessionStorage.getItem("token")){
    // token的值  放到请求头里面
    let token = sessionStorage.getItem("token");
    config.headers['token']=token;
  }
  // config.headers['Authorization']="yyl"

  return config;
}, error=> {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error);
});



// 添加响应拦截器
instance.interceptors.response.use( response=> {
  console.log(response)
  // 状态码  500
  if(response.data.code!=200){
    alert("chucuole")
    console.log(response.data);

    router.push({path:"/login"});

    return;
  }
  return response;
}, error=> {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error);
});

Vue.prototype.$axios = instance;
// 引入组件

挂载点

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

4.搭建一个.vue页面,并在 router 目录下的 index.js 文件配置好路由

<template>
  <div class="login-container">
    <el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="login-form">
      <el-form-item label="用户名" prop="username">
        <el-input type="text" v-model="ruleForm.username" autocomplete="off"></el-input>
      </el-form-item>
      <el-form-item label="确认密码" prop="password">
        <el-input type="password" v-model="ruleForm.password" autocomplete="off"></el-input>
      </el-form-item>


      <el-form-item>
        <el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
        <el-button @click="resetForm('ruleForm')">重置</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

搭建的页面包含基本的登录表单,在新建一个页面用于成功的页面展示,如 图中跳转的main.vue

 methods: {
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          alert('submit!');
          // 请求  userlogin   userlogin
          //post i请求  json 数据  后端接受的时候  @RequestBody
          this.$axios.post("userlogin",qs.stringify(this.ruleForm)).then(r=>{
            // 获取token的值
            console.log(r.data.t);
            // 存起来
            sessionStorage.setItem("token",r.data.t)
            // 成功之后    跳转 /main
            this.$router.push("/main");

            //console.log(r.data);
          })


        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
  }
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/login',
    name: 'login',
    component: () => import(/* webpackChunkName: "about" */ '../views/login.vue')
  },
  {
    path: '/about',
    name: 'about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  },
  {
    path: '/main',
    name: 'main',
    component: () => import(/* webpackChunkName: "about" */ '../views/main.vue')
  },
]
// 针对ElementUI导航栏中重复导航报错问题
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
  return originalPush.call(this, location).catch(err => err)
}

这里配置了导航重复导航的问题,我们在响应拦截器配置了code非200的跳转登录的情况,为了避免登录失败导致跳转登录页面,重复导航的问题

5.后端加入跨域的配置文件

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 CrossConfig {
    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration corsConfiguration = new CorsConfiguration();
        //corsConfiguration.setAllowCredentials(true);  // 允许 携带cookie 的信息
        corsConfiguration.addAllowedHeader("*"); // 允许所有的头
        corsConfiguration.addAllowedOrigin("*");// 允许所有的请求源
        corsConfiguration.addAllowedMethod("*");  // 所欲的方法   get post delete put
        source.registerCorsConfiguration("/**", corsConfiguration); // 所有的路径都允许跨域
        return new CorsFilter(source);
    }

}

6.统一返回数据实体

@Data
@AllArgsConstructor //
@NoArgsConstructor //
public class Result<T> {
    /**
     * code编码
     */
    private Integer code = 200;
    /**
     * 消息
     */
    private String msg = "操作成功";
    /**
     * 具体的数据
     */
    private T t;

    /**
     * 成功的静态方法
     */
    public static <T> Result  success(T t){
        return new Result<>(200,"操作成功",t);
    }

    public static <T>  Result  <T>  fail(){
        return new Result<>(500,"操作失败",null);
    }

    public static <T>  Result  <T>  forbidden(){
        return new Result<>(403,"权限不允许",null);
    }
}

7.对实现了UserDetailsService接口的service层进行了修改

@Service
public class MyUserDetailService implements UserDetailsService {
    @Resource
    private TabUserMapper userMapper;
    @Resource
    private TabUserRoleMapper userRoleMapper;
    @Resource
    private TabRoleMapper roleMapper;
    @Resource
    private TabMenuMapper menuMapper;

//    根据用户的名字 加载用户的信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        username 代表前端传递过来的名字
//        根据名字去数据库查询一下有没有这个用户的信息
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("username",username);
        TabUser tabUser = userMapper.selectOne(queryWrapper);
        if(tabUser != null) {
//            有值 查询用户对应的角色的id
            QueryWrapper queryWrapper1 = new QueryWrapper();
            queryWrapper1.eq("uid",tabUser.getId());
            List<TabUserRole> tabUserRoles = userRoleMapper.selectList(queryWrapper1);
            List<Integer> rids = tabUserRoles.stream().map(tabUserRole -> tabUserRole.getRid()).collect(Collectors.toList());
//            根据角色的id 查询rcode
            List<TabRole> tabRoles = roleMapper.selectBatchIds(rids);
//            角色的修信息 角色管理 修改角色的名字
            List<SimpleGrantedAuthority> collect = tabRoles.stream().map(tabRole -> new SimpleGrantedAuthority("ROLE_" + tabRole.getRcode())).collect(Collectors.toList());
//            根据角色的id 查询菜单的mcode
            List<TabMenu> menus = menuMapper.selectCodeByRids(rids);
            List<SimpleGrantedAuthority> resources = menus.stream().map(tabMenu -> new SimpleGrantedAuthority(tabMenu.getMcode())).collect(Collectors.toList());
//            将角色的所有信息,和资源信息合并在一起
            List<SimpleGrantedAuthority> allresource = Stream.concat(collect.stream(), resources.stream()).collect(Collectors.toList());
            return new User(username, tabUser.getPassword(), allresource);
        }
        return null;
    }
}

8.数据链路层,对前后端的认证进行判断与返回的JSON数据

@Component
public class JwtFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        /*解析token
        1.获取token -> 存在 -> 解析
          不存在返回 null 没有认证
        2.效验token真的还是假的 真-> 过 -> 用户的信息存放到安全框架的上下文路径里面
          假-> 返回一个Json 数据 没有认证
        * */
        String[] whitename = {"/userlogin"};
        String token = request.getHeader("token");
//        token存在
        if(StringUtils.isNotBlank(token)) {
//            存在 解析
            boolean verify = JWTUtil.verify(token, "hp".getBytes());
            if(verify) {
//                效验合格
//                获取用户的名字 和密码的信息
                JWT jwt = JWTUtil.parseToken(token);
                String username = (String) jwt.getPayload("username");
                List<String> resources = (List<String>) jwt.getPayload("resources");
//                资源的信息
                List<SimpleGrantedAuthority> collect = resources.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList());
//                保存用户的信息
                UsernamePasswordAuthenticationToken usertoken = new UsernamePasswordAuthenticationToken(username, null, collect);
//                存起来用户的信息
                SecurityContextHolder.getContext().setAuthentication(usertoken);
//                放行
                filterChain.doFilter(request,response);
            }else  {
                Result result = new Result(401, "没有登录", null);
                printJsonData(response,result);
            }
        }else {
//              查看是否在白名单 如果在 就放行
            String requestURL = request.getRequestURI();
            if(ArrayUtils.contains(whitename,requestURL)) {
                filterChain.doFilter(request,response);
            }else {
                Result result = new Result(401, "没有登录", null);
                printJsonData(response,result);
            }
        }
    }
    public void printJsonData(HttpServletResponse response, Result result) {
        try {
            response.setContentType("application/json;charset=utf8"); //json格式 编码是中文
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(result);// 使用objectMapper将result转化为json字符串
            PrintWriter writer = response.getWriter();
            writer.print(s);
            writer.flush();
            writer.close();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

9.对config文件进行修改(前后端分离情况)

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private JwtFilter jwtFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //    配置 登录form 表单
//    路劲前面必须加 /
        http.formLogin()
                .loginProcessingUrl("/userlogin")
                .successHandler((request, response, authentication) -> {
                    System.out.println("authentication"+authentication);
//                    资源的信息
                    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
                    List<String> allresources = authorities.stream().map(s -> s.getAuthority()).collect(Collectors.toList());
                    System.out.println("allresources"+allresources);
//                    认证成功
//                    生成token
                    Map map =new HashMap<>();
                    map.put("username",authentication.getName());  // 认证成功之后 用户的名字
                    map.put("resources",allresources);
//                    资源的信息
                    设置签发时间
//                    Calendar instance = Calendar.getInstance(); //获取当前的时间
//                    Date time = instance.getTime();
                    过期的时间设置为2小时之后
//                    instance.add(Calendar.HOUR,2); //两个小时之后
//                    Date time1 = instance.getTime();
//                    map.put(JWTPayload.EXPIRES_AT,time1);
//                    map.put(JWTPayload.ISSUED_AT,time);
//                    map.put(JWTPayload.NOT_BEFORE,time);
                    String token = JWTUtil.createToken(map, "hp".getBytes());
                    System.out.println(token);
                    Result result = new Result(200,"登录成功",token);
                    printJsonData(response,result);
                }) //前后端分离的时候 认证成功 走的方法
                .failureHandler((request, response, exception) -> {
                    Result result = new Result(500, "失败", null);
                    printJsonData(response,result);
                }); //认证失败 走的方法
        http.authorizeRequests().antMatchers("/userlogin").permitAll(); //代表放行 "/userlogin"
        http.authorizeRequests().anyRequest().authenticated();
//        权限不允许的时候
        http.exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> {
            Result result = new Result(403, "权限不允许", null);
            printJsonData(response,result);
        });
        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
//         csrf 方便html文件 能够通过
        http.csrf().disable();
        http.cors();   // 可以跨域

    }
    @Resource
    private UserDetailsService userDetailsService;

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

    //    自定义用户的信息
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(getPassword());
    }
    public void printJsonData(HttpServletResponse response, Result result) {
        try {
            response.setContentType("application/json;charset=utf8");
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(result);
            PrintWriter writer = response.getWriter();
            writer.print(s);
            writer.flush();
            writer.close();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

10.配置完成

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Django快速入门(一)

Django三板斧 1. 基本使用 三板斧: HttpResponse,render,redirect from django.shortcuts import HttpResponse,render,redirect# 一. 返回字符串类型的数据 return HttpResponse(字符串) # 二. 返回HTML文件 # 1. 动态HTML页面: return render(request,login.html) def ab…

极智开发 | CUDA线程模型与全局索引计算方式

欢迎关注我的公众号 [极智视界],获取我的更多经验分享 大家好,我是极智视界,本文分享一下 CUDA线程全局索引计算方式。 邀您加入我的知识星球「极智视界」,星球内有超多好玩的项目实战源码和资源下载,链接:https://t.zsxq.com/0aiNxERDq CUDA 线程全局索引的计算,是很容…

图文加多个测试带你彻底搞懂Netty ChannelPipeline的执行顺序(附源码)

这里是weihubeats,觉得文章不错可以关注公众号小奏技术&#xff0c;文章首发。拒绝营销号&#xff0c;拒绝标题党 netty version 4.1.65.Final ChannelPipeline 是什么 Pipeline&#xff0c;管道、流水线&#xff0c;类似于责任链模式。基本上我们使用Netty开发程序需要编写…

Apache RocketMQ - 概述

2022年&#xff0c;RocketMQ 5.0的正式版发布&#xff0c;相比于4.0版本而言&#xff0c;架构走向云原生化&#xff0c;并且覆盖了更多的业务场景。 如何从互联网时代演进到云时代&#xff1f; 1. 消息队列演进史 操作系统、数据库、中间件是基础软件的三驾马车&#xff0c;…

Sketch是什么软件,如何收费和获得免费版

Sketch软件为设计师构建了一个优秀的本地Mac应用程序。Sketch是整个设计过程的平台&#xff0c;通过基于Web的工具共享工作&#xff0c;获取反馈&#xff0c;测试原型&#xff0c;并将其移交给任何浏览器。Sketch软件的定价根据不同的许可类型和订阅计划而变化。本文从Sketch软…

18. 深度学习 - 从零理解神经网络

文章目录 本文目标预测趋势与关系波士顿房价预测 Hi, 你好。我是茶桁。 我们终于又开启新的篇章了&#xff0c;从今天这节课开始&#xff0c;我们会花几节课来理解一下深度学习的相关知识&#xff0c;了解神经网络&#xff0c;多层神经网络相关知识。并且&#xff0c;我们会尝…

网络安全自学手册

想自学网络安全&#xff08;黑客技术&#xff09;首先你得了解什么是网络安全&#xff01;什么是黑客&#xff01; 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全…

No module named ‘importlib.metadata‘

解决办法 参考博客 https://wenku.csdn.net/answer/45a1563cc02e9592dd1d1d28fe7b88e7 pip install importlib_metadata

使用U盘安装ubuntu22操作教程

U盘启动 将烧录好的U盘&#xff0c;插上待安装系统的电脑 服务器在开机之后长按【ESC键】进入BIOS选项中&#xff0c;选择对应的U盘启动 如下图&#xff0c;在界面中“USB”选项就是我的U盘&#xff0c;第一启动项选择U盘启动&#xff0c;其他启动项不动&#xff0c;选择后按F…

③【操作表数据】MySQL添加数据、修改数据、删除数据

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ MySQL添加数据、修改数据、删除数据 &#x1f…

让旗下产品受到更多用户认可,GNC健安喜登陆中国国际进口博览会

11月5日-10日&#xff0c;第六届中国国际进口博览会&#xff08;以下简称“中国进博会”&#xff09;在上海国家会展中心正式起航。自2018年首次举办以来&#xff0c;中国进博会受到了无数参展企业的推崇&#xff0c;无数制造商、采购商的追捧。随着参会企业的逐年增长&#xf…

辐射骚扰整改思路及方法:对共模电流的影响?|深圳比创达电子EMC

某产品首次EMC测试时&#xff0c;辐射、静电、浪涌均失败。本篇文章就“原理探究&#xff1a;对共模电流的影响”问题进行详细讨论。 现在来研究左侧的磁场分布情况。分别对两根导线使用右手螺旋定则可以发现&#xff0c;两根导线的磁场均为顺时针方向&#xff0c;即磁场是互相…

Java后端开发——JDBC入门实验

JDBC&#xff08;Java Database Connectivity&#xff09;是Java编程语言中用于与数据库建立连接并进行数据库操作的API&#xff08;应用程序编程接口&#xff09;。JDBC允许开发人员连接到数据库&#xff0c;执行各种操作&#xff08;如插入、更新、删除和查询数据&#xff09…

代码随想录 Day38 完全背包问题 LeetCode T70 爬楼梯 T322 零钱兑换 T279 完全平方数

前言 在今天的题目开始之前,让我们来回顾一下之前的知识,动规五部曲 1.确定dp数组含义 2.确定dp数组的递推公式 3.初始化dp数组 4.确定遍历顺序 5.打印dp数组来排错 tips: 1.当求取物品有限的时候用0-1背包,求取物品无限的时候用完全背包 结果是排列还是组合也有说法,当结果是组…

如何选择最适合的知识付费小程序开发工具?

在选择适合的知识付费小程序开发工具时&#xff0c;需要考虑开发者的技能水平、项目需求、平台兼容性以及用户体验。下面将介绍一些常用的开发工具&#xff0c;并提供一些选择工具的考虑因素。 1. 微信小程序开发工具 微信小程序是知识付费小程序的一个常见平台&#xff0c;…

生活污水处理一体化处理设备有哪些

生活污水处理一体化处理设备有多种类型&#xff0c;包括但不限于以下几种&#xff1a; 鼓风机&#xff1a;提供曝气系统所需的气流。潜水污水提升泵&#xff1a;将污水从低处提升到高处。旋转式滚筒筛分机&#xff1a;对污水中的悬浮物进行分离和筛选。回旋式格栅&#xff1a;…

LVS NAT 模式

1.3.2. LVS DR 模式 模式&#xff08;局域网改写 &#xff08;局域网改写 mac 地址&#xff09; ①.客户端将请求发往前端的负载均衡器&#xff0c;请求报文源地址是 CIP&#xff0c;目标地址为 VIP。 ②.负载均衡器收到报文后&#xff0c;发现请求的是在规则里面存在的地址&am…

股票四倍杠杆什么意思?

股票四倍杠杆是指投资者通过借款或使用金融衍生品&#xff0c;以增加其投资股票的能力&#xff0c;达到放大投资回报的目的。具体来说&#xff0c;投资者可以通过向券商或银行等金融机构借入资金&#xff0c;或者使用融资融券等金融衍生品&#xff0c;以增加其购买股票的资本&a…

看李广的故事:发现团队管理之道

在漠北之战中&#xff0c;李广因迷失道路而延误了军期。因李广年事已高&#xff0c;无法承受幕府的责难&#xff0c;最终选择在军前自刎而死。 这一事件令人痛惜&#xff0c;不禁让人想起在工作中遇到的类似情况。有些同事因为突然离职&#xff0c;让领导感到愕然&#xff0c;…

【python小游戏】飞机大作战源码分享(附完整源码+图片资源可直接运行)

效果演示 源码 plane_main1.py import pygame from plane_sprites import * import timeclass PlaneGame(object):"""飞机大战主游戏"""def __init__(self):print("游戏初始化")# 1. 创建游戏的窗口self.screen pygame.display.set…