zoukankan      html  css  js  c++  java
  • 63

    简介
    登录模块很简单,前端发送账号密码的表单,后端接收验证后即可~

    淦!可是我想多了,于是有了以下几个问题(里面还包含网络安全问题):

    1.登录时的验证码

    2.自动登录的实现

    3.怎么维护前后端登录状态

    在这和大家分享下我实现此功能的过程,包括一些技术和心得

    1.登录时的验证码
    为什么要验证码,原因很简单,防止脚本无限次重复登录,来暴力破解用户密码或者攻击服务器

    验证码的出现,使得每次登录都有个动态变量需要输入,无法用脚本写死代码

    具体可以参考:滑动验证码的设计和理解

    2.自动登录的实现
    所谓自动登录,指的是当用户登录网站时勾选了自动登录,那么下次再访问网站就不需要输入账号密码直接登录了

    这说明,账号密码信息是必须保存在用户这边的,因此自动登录都是不安全的!(方便的代价呀)

    尽管不安全,但是我们也必须要尽力让它安全一点,有以下常用方法:

    1.账号密码加密保存

    2.降低自动登录后用户的权限(如果用户自动登录想改密码,想给我转钱等操作的话,就必须输入账号密码再登录一次!)

    3.进行ip检测(之前登录的ip小本本记着),如果发现和上次不一致,则不允许自动登录

    数据存储在前端哪里呢
    浏览器有3个经常保存数据的地方

    1.Cookie (我用这个)

    2.LocalStorage

    3.SessionStorage

    各位可以按F12直接观看

    如果你在多个大型网站下都按按F12,会发现SessionStorage基本没数据

    为啥,因为真的不好用,它并不是后台的session那样,生命周期是一个会话,这个SessionStorage存储的数据只限于该标签的页面

    意思是标签1和标签2即使是同个URL的网址,里面的数据都是不互通的(这有个毛用)

    那么LocalStorage存储的数据如何呢,答案是无限期本地存储

    不过后台无法操作这里的数据,只能由js代码操作(至于操作结果,完全看js,后端无法感知,不太可靠),我认为这里不适合保存敏感点的信息,因为前端的功能是展示,状态性的数据应该由后端直接掌控(后端能直接操作Cookie,保证完成任务)

    你看英雄所见略同,CSDN网站的用户密码也是存在Cookie的

    Token就是登录后的令牌(下一点会讲)

    所以用Cookie就对啦,具体实现都很简单,前端多个自动登录的选择,选择后多个参数传给后端,后端根据参数往Cookie里设置加密后的账号密码

    等下次访问时,用拦截器Interceptor进行拦截,检测是否要自动登录即可~

    3.如何维护前后端登录状态
    大家最先想到是用Session来维护,登录后在Session中存放用户信息,不过对分布式很不友好(什么,你说你用不到分布式,我也没用到,可是梦想还是要有的嘛),需要维护个分布式数据库来进行数据同步才行

    于是我用Token实现的,Token就是一串字符串,最适合API鉴权(例如SSO单点登录这种),俗称令牌

    好处就是账号密码用户输入一次就够了,特别是多个系统之间(一张身份的凭证都通用)

    当用户登录后,服务器就会生成一个Token放在Cookie中,之后用户的所有操作都带这个Token访问(将Token放入http头部)

    为什么要将Token放入头部
    1.能抵挡下简单的CSRF攻击

    2.浏览器跨域问题

    什么是CSRF攻击
    举个例子:我登录了A网站,A网站给我返回了一些Cookie信息,然后我再同一浏览器的另外标签访问了B网站,谁知这个B网站返回了一些攻击代码(向A网站发起一些请求,比如转钱给你,这时候由于是访问A网站,会附带A网站的Cookie,让一切都好像是我在访问一样),这个就是CSRF攻击

    但B网站并不知道A网站这么鸡贼,会在头部放了Token,所以这次攻击请求是的头部是没Token的,因此检测后发现非法,所以没得逞

    当然,这并不可靠,哪天B网站知道你头部放了Token,它研究A网站的js代码,清楚逻辑之后也加上,那就防不住了(所以说前端的东西一切都不可靠)

    正确做法应该是后端检测头部的Referer字段,每个网页里发起请求,请求的头部都会带有此字段,如

    这说明这个请求是从 http://localhost:8099/swr 中发出的

    B网站如果返回攻击代码,这里显示的事B网站的网址,判断出不是自家网站发出,就可以禁止访问

    浏览器跨域访问会发生什么
    说到跨域(自家网站去请求别人家的网站),得先了解什么是同源策略:

    同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

    它的核心就在于它认为自任何站点装载的信赖内容是不安全的。当被浏览器半信半疑的脚本运行在沙箱时,它们应该只被允许访问来自同一站点的资源,而不是那些来自其它站点可能怀有恶意的资源。

    所谓同源是指:域名、协议、端口相同。

    下表是相对于 http://www.laixiangran.cn/home/index.html 的同源检测结果:

    另外,同源策略又分为以下两种:

    DOM 同源策略:禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
    XMLHttpRequest 同源策略:禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。(就是ajax)
    咳咳,这里要说下第二种,其实设置一些参数之后,ajax访问时允许跨域请求的,甚至允许跨域时带上自身cookie

    但是,带上自己的Cookie多不安全,明明里面只有1,2个信息要传给对方,现在被人全看见了(不好不好),所以要将Token放入头部

    你说为啥不放到参数里,因为这会跟业务用的参数混淆,造成逻辑混乱(就好像你上学时要扔家里的垃圾,你不会放到书包里吧,都是手里提着的)

    每个请求都放token,所以要封装起来,例如我是将ajax封装起一个新的对象,然后在这个对象使用时添加Token

    当然啦,封装了ajax后还有其他好处(例如统一的成功,失败回调函数,统一的数据解析,统一的等待框等等),有兴趣的同学可以看下

    View Code
    预防XSS攻击,Filter知识讲解
    网上有些文章说,后端设置HttpOnly,让Cookie无法让js读写,可以防止XSS攻击。

    (⊙o⊙)…简直就是乱写,首先要了解下什么是XSS攻击

    Xss攻击是什么
    举个简单的例子,假设你前端有个地方可以输入,然后保存的数据库的地方

    用户A输入了以下东西

    然后这东西就到了后台,当作一串字符串保存了起来

    刚好你网站的html代码里,有个地方是显示用户输入过的东西的(例如评论区),然后上面的东西就被加载到html里面,如

    接下来每个人打开你的网站,都会弹出123的对话框,这就是XSS攻击

    怎么预防呢,在后端设置过滤器,对输入进行过滤,先上代码

    1 /**
    2 * @auther: NiceBin
    3 * @description: 系统的拦截器,注册在FilterConfig类中进行
    4 * 不能使用@WebFilter,因为Filter要排序
    5 * 1.对ServletRequest进行封装
    6 * 2.防止CSRF,检查http头的Referer字段
    7 * @date: 2020/12/15 15:32
    8 */
    9 @Component
    10 public class SystemFilter implements Filter {
    11 private final Logger logger = LoggerFactory.getLogger(SystemFilter.class);
    12 @Autowired
    13 private Environment environment;
    14
    15 @Override
    16 public void init(FilterConfig filterConfig) throws ServletException {
    17 logger.info(“系统拦截器SystemFilter开始加载”);
    18 }
    19
    20 @Override
    21 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    22 SystemHttpServletRequestWrapper requestWrapper = new SystemHttpServletRequestWrapper((HttpServletRequest) request);
    23
    24 //检测http的Referer字段,不允许跨域访问
    25 String hostPath = environment.getProperty(“server.host-path”);
    26 String referer = requestWrapper.getHeader(“Referer”);
    27 if(!Tool.isNull(referer)){
    28 if(referer.lastIndexOf(hostPath)!=0){
    29 ((HttpServletResponse)response).setStatus(HttpStatus.FORBIDDEN.value()); //设置错误状态码
    30 return;
    31 }
    32 }
    33 chain.doFilter(requestWrapper,response);
    34 }
    35
    36 @Override
    37 public void destroy() {
    38
    39 }
    40 }
    乍一看,是不是没发现哪里预防了XSS,其实正在的关键点在22行和33行代码,里面的SystemHttpServletRequestWrapper类才是关键,这个类是包装类,是替换参数里的ServletRequest类的,为的就是重写里面的方法,来达到预防XSS的目的,因为Spring也是根据ServletRequest类来进行前端参数读取的,所以它就是后端获得数据的源头

    1 /**
    2 * @auther: NiceBin
    3 * @description: 包装的httpServlet,进行以下增强
    4 * 1.将流数据取出保存,方便多次读出
    5 * 2.防止XSS攻击,修改读取数据的方法,过滤敏感字符
    6 * @date: 2020/4/23 19:50
    /
    8 public class SystemHttpServletRequestWrapper extends HttpServletRequestWrapper {
    9 private final byte[] body;
    10 private HttpServletRequest request;
    11
    12 public SystemHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
    13 super(request);
    14 //打印属性
    15 //printRequestAll(request);
    16 body = HttpHelper.getBodyString(request).getBytes(Charset.forName(“UTF-8”)); //HttpHelper是我自己写的工具类
    17 this.request = request;
    18 }
    19
    20 @Override
    21 public BufferedReader getReader() throws IOException {
    22 return new BufferedReader(new InputStreamReader(getInputStream()));
    23 }
    24
    25 @Override
    26 public ServletInputStream getInputStream() throws IOException {
    27 final ByteArrayInputStream bais = new ByteArrayInputStream(body);
    28 return new ServletInputStream() {
    29 @Override
    30 public boolean isFinished() {
    31 return false;
    32 }
    33
    34 @Override
    35 public boolean isReady() {
    36 return false;
    37 }
    38
    39 @Override
    40 public void setReadListener(ReadListener readListener) {
    41
    42 }
    43
    44 @Override
    45 public int read() throws IOException {
    46 return bais.read();
    47 }
    48 };
    49 }
    50
    51 /
    *
    52 * 可以打印出HttpServletRequest里属性的值
    53 * @param request
    54 */
    55 public void printRequestAll(HttpServletRequest request){
    56 Enumeration e = request.getHeaderNames();
    57 while (e.hasMoreElements()) {
    58 String name = (String) e.nextElement();
    59 String value = request.getHeader(name);
    60 System.out.println(name + " = " + value);
    61 }
    62 }
    63
    64 //以下为XSS预防
    65 @Override
    66 public String getParameter(String name) {
    67 String value = request.getParameter(name);
    68 if (!StringUtils.isEmpty(value)) {
    69 value = StringEscapeUtils.escapeHtml4(value);
    70 }
    71 return value;
    72 }
    73
    74 @Override
    75 public String[] getParameterValues(String name) {
    76 String[] parameterValues = super.getParameterValues(name);
    77 if (parameterValues == null) {
    78 return null;
    79 }
    80 for (int i = 0; i < parameterValues.length; i++) {
    81 String value = parameterValues[i];
    82 parameterValues[i] = StringEscapeUtils.escapeHtml4(value);
    83 }
    84 return parameterValues;
    85 }
    86 }
    HttpHelper工具类:

    View Code
    可以看到SystemHttpServletRequestWrapper的64行开始,重写了两个获取参数的方法,在获取参数的时候进行过滤即可~

    那64行往上是干啥的咧,这个是将ServletRequest里的数据读出来保存一份,因为ServletRequest里的数据流只能读取一次,很不方便

    啥意思呢,就是你在这个Filter里

    inputStream = request.getInputStream();
    reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName(“UTF-8”)));
    String line = “”;
    while ((line = reader.readLine()) != null) {
    sb.append(line);
    }
    把数据读完,下个Filter再执行这些代码,就没数据了(从而导致Spring也接收不到数据)

    所以要保存起来,让后面的过滤器Filter和拦截器Interceptor快乐的读数据,没有后顾之忧(例如上面提到的验证码设计,如果你想用拦截器拦截,然后进行验证,则势必会读数据),既然封装ServletRequest这么重要,那必须得保证这个Filter第一个加载啊

    在Springboot中,Filter的排序用@Order是没用的,必须要用FilterRegistrationBean进行注册才能排序,如:

    1 /**
    2 * @auther: NiceBin
    3 * @description: 为了排序Filter,如果Filter有顺序要求
    4 * 那么需要在此注册,设置order(值越低优先级越高)
    5 * 其他没顺序需要的,可以@WebFilter注册
    6 * 如@WebFilter(filterName = “SecurityFilter”, urlPatterns = “/*”, asyncSupported = true)
    7 * @date: 2020/12/15 15:48
    /
    9 @Configuration
    10 public class FilterConfig {
    11
    12 @Autowired
    13 SystemFilter systemFilter;
    14 /
    *
    15 * 注册SystemFilter,顺序为1,任何其他filter不能比他优先
    16 * @return
    17 /
    18 @Bean
    19 public FilterRegistrationBean filterRegist(){
    20 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
    21 filterRegistrationBean.setFilter(systemFilter);
    22 filterRegistrationBean.setName(“SystemFilter”);
    23 filterRegistrationBean.addUrlPatterns("/
    ");
    24 filterRegistrationBean.setAsyncSupported(true);
    25 filterRegistrationBean.setOrder(1);
    26 return filterRegistrationBean;
    27 }
    28 }
    当然了,如果你没用Springboot,那web.xml中定义的顺序就是Filter加载的顺序

    知识点提问:在我们之后的Filter或者Interceptor中,需要
    1 SystemHttpServletRequestWrapper requestWrapper = (SystemHttpServletRequestWrapper) request
    这样强制转换才能用吗?

    答案是不用的,你可以想想Spring也用了这个东西的,它怎么知道你定义的类叫什么名字,怎么强制转换,那么这设计到Java什么知识呢

    没错,就是Java的多态性,我们看以下代码

    public class Father {
    public void sayName(){
    System.out.println(“我是爸爸”);
    }
    }

    public class Son extends Father{
    public void sayName(){
    System.out.println(“我是儿子”);
    }
    }

    public class Test {

    @org.junit.Test
    public void test() throws Exception {
        Father father = new Son();
        otherMethod(father);
    }
    
    public void otherMethod(Father father){
        father.sayName();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    }
    输出:我是儿子

    答错了的留言,看看有多少小伙子~~ 接下来言归正传

    选择JWT生成Token
    JWT全称JSON Web Tokens 是一种规范化的 token(别人想的挺多挺全面的了,比你自己想的token要好一点)

    一个 JWT token 是一个字符串,它由三部分组成,头部、载荷与签名,中间用 . 分隔,例如:xxxxx.yyyyy.zzzzz

    头部(header)
    头部通常由两部分组成:令牌的类型(即 JWT)和正在使用的签名算法(如 HMAC SHA256 或 RSA.)。

    例如:

    {
    “alg”: “HS256”,
    “typ”: “JWT”
    }
    然后用 Base64Url 编码得到头部,即 xxxxx。Base64Url编码后,才能在URL中正常传输(因为有人会把Token放在URL里…)

    载荷(Payload)
    载荷中放置了 token 的一些基本信息,以帮助接受它的服务器来理解这个 token。同时还可以包含一些自定义的信息,用户信息交换,如:

    {
    “sub”: “1”,

    “iss”: “http://localhost:8000/auth/login”,

    “iat”: 1451888119,

    “exp”: 1454516119,

    “nbf”: 1451888119,

    “jti”: “37c107e4609ddbcc9c096ea5ee76c667”,

    “aud”: “dev”

    }
    可以将载荷用别的方式加密一遍,这样别人得到了token也看不懂

    签名(Signature)
    签名时需要用到前面编码过的两个字符串,如果以 HMACSHA256 加密,就如下:

    HMACSHA256(

    base64UrlEncode(header) + "." +
    
    base64UrlEncode(payload),
    
    secret
    
    • 1
    • 2
    • 3
    • 4
    • 5

    )
    加密后再进行 base64url 编码最后得到的字符串就是 token 的第三部分 zzzzz。

    组合便可以得到 token:xxxxx.yyyyy.zzzzz。

    签名的作用:保证 JWT 没有被篡改过,原理如下:

    HMAC 算法是不可逆算法,类似 MD5 和 hash ,但多一个密钥,密钥(即上面的 secret)由服务端持有,客户端把 token 发给服务端后,服务端可以把其中的头部和载荷再加上事先共享的 secret 再进行一次 HMAC 加密,得到的结果和 token 的第三段进行对比,如果一样则表明数据没有被篡改。

    具体Java使用:

    com.auth0 java-jwt 3.10.2 io.jsonwebtoken jjwt 0.9.1 1 ** 2 * @auther: NiceBin 3 * @description: Jwt构造器,创建Token来进行身份记录 4 * jwt由3个部分构成:jwt头,有效载荷(主体,payLoad),签名 5 * @date: 2020/5/7 22:40 6 */ 7 public class JwtTool { 8 9 //以下为JwtTool生成时的主题 10 //登录是否还有效 11 public static final String SUBJECT_ONLINE_STATE = "online_state"; 12 13 //以下为载荷固定的Key值 14 //主题 15 public static final String SUBJECT = "subject"; 16 //发布时间 17 public static final String TIME_ISSUED = "timeIssued"; 18 //过期时间 19 public static final String EXPIRATION = "expiration"; 20 21 /** 22 * 生成token,参数都是载荷(自定义内容) 23 * 其中Map里为非必要数据,而其他参数为必要参数 24 * 25 * @param subject 主题,token生成干啥用的,用上面的常量作为参数 26 * @param liveTime 存活时间(秒单位),建议使用TimeUnit方便转换 27 * 如TimeUnit.HOURS.toSeconds(1);将1小时转为秒 = 3600 28 * @param claimMap 自定义荷载,可以为空 29 * @return 30 */ 31 public static String createToken(String subject, long liveTime, HashMap

    Https防止半路被截和重放攻击
    前面提到了Token就是身份令牌,可以相当于已登录一样进入系统,那么半路被人截了那就不好了

    所以要用Https协议,具体怎么设置大家自行百度吧(直接在tomcat操作的,不需要更改代码,证书也有免费的~)

    这里说下Https建立连接的过程,来看看为什么就不会被人截获了

    1.服务器先向CA(证书颁布机构)申请一个证书(证书里有自己的ip等等消息),然后在自己服务器设置好

    2.浏览器向服务器发送HTTPS请求,服务器将自己的证书发给浏览器

    3.浏览器拿到证书后,查看证书是否过期啊,ip是不是跟服务器的一样啊,跟检查身份证跟你长得像不像一样,检查没问题后,跟自己系统里的CA列表比对,看看是谁发的(找不到就报错,说证书不可信),比对成功后从列表里拿出对应的CA公钥解密证书(具体方法跟JWT的很像,浏览器用相同的算法和公钥对证书部分进行加密,看得到的值和证书的签名是否一致),得到服务器的公钥

    4.然后生成一个传输私钥,用服务器的公钥加密,发给服务器

    5.服务器用服务器的私钥解密,得到了传输秘钥,然后用传输秘钥进行加密要传送的信息发给浏览器

    6.浏览器用秘钥解密,然后用传输秘钥进行加密要传送的信息发给服务器(对称加密)

    7.重复5,6步骤直到结束

    以上哪个步骤黑客得到数据都看不懂

    至于为什么能防重放攻击,是因为Https通信自带序列号,如果黑客截取了浏览器的请求,重复发送一遍,那么序列号会一样,会被直接丢弃

  • 相关阅读:
    Gym 100818F Irrational Roots (数学)
    学习总结 for循环--冒泡排序
    学习总结 for循环语句的应用
    学习总结 条件语句的应用
    学习总结 运算符了解与应用
    学习记录 彻底搞清 C#中a++与++a的区别
    学习总结 数据类型的应用与转换
    学习总结 数据类型
    学习总结 二进制转换与应用
    学习总结 vs软件简单了解
  • 原文地址:https://www.cnblogs.com/gd11/p/14217484.html
Copyright © 2011-2022 走看看