SpringSecurity从入门到精通:从数据库查询权限信息&自定义失败处理

Posted YuFu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity从入门到精通:从数据库查询权限信息&自定义失败处理相关的知识,希望对你有一定的参考价值。

从数据库查询权限信息

 

 

 

 

 

 

记得打开redis

 

 

 

 

 

 

自定义失败处理

  我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。

​   在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

​   如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

​   如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

  ​ 所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

 

AuthenticationEntryPointImpl

package com.sangeng.handler;

import com.alibaba.fastjson.JSON;
import com.sangeng.Utils.WebUtils;
import com.sangeng.domain.ResponseResult;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint 
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException 
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);
    

 

AccessDeniedHandlerImpl

package com.sangeng.handler;

import com.alibaba.fastjson.JSON;
import com.sangeng.Utils.WebUtils;
import com.sangeng.domain.ResponseResult;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler 
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException 
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);

    

 

 

SecurityConfig

package com.sangeng.config;

import com.sangeng.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter 


    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;


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

    @Override
    protected void configure(HttpSecurity http) throws Exception 
       /* http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();*/
       http
               .csrf().disable()
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
               .and()
               .authorizeRequests()
               .antMatchers("/hello").permitAll()
               .antMatchers("/user/login").anonymous()
               .anyRequest().authenticated();

       //添加过滤器
        http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //配置异常处理器
        http.exceptionHandling()
                //配置认证失败处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
    

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception 
        return super.authenticationManagerBean();
    

 

 

 

WPF MVVM从入门到精通8:数据验证

原文:WPF MVVM从入门到精通8:数据验证

WPF MVVM从入门到精通1:MVVM模式简介

WPF MVVM从入门到精通2:实现一个登录窗口

WPF MVVM从入门到精通3:数据绑定

WPF MVVM从入门到精通4:命令和事件

WPF MVVM从入门到精通5:PasswordBox的绑定

WPF MVVM从入门到精通6:RadioButton等一对多控件的绑定

WPF MVVM从入门到精通7:关闭窗口和打开新窗口

WPF MVVM从入门到精通8:数据验证

完整示例代码下载LoginDemo

到目前为止,登录窗口的基本功能似乎都完成了。但我们知道,很多时候用户名的格式是有要求的,例如是只有字母数字下划线,或者字数有限制。这要求我们在登录之前,验证输入内容的正确性。在这一节,我们需要验证用户名和密码的正确性,如果上面两个框的输入非法,禁用登录按钮。

在数据验证错误的时候,我们显示一个叹号在输入框的旁边,如下图所示:

技术图片

数据验证的方法有很多,我们使用了一种比较优雅的。

首先定义一些验证属性:

using System.ComponentModel.DataAnnotations;

namespace LoginDemo.ViewModel.Login
{
    public class NotEmptyCheck : ValidationAttribute
    {
        public override bool IsValid(object value)
        {
            var name = value as string;
            if (string.IsNullOrEmpty(name))
            {
                return false;
            }
            return true;
        }

        public override string FormatErrorMessage(string name)
        {
            return "不能为空";
        }
    }

    public class UserNameExists : ValidationAttribute
    {
        public override bool IsValid(object value)
        {
            var name = value as string;
            if (name.Contains("abc"))
            {
                return true;
            }
            return false;
        }

        public override string FormatErrorMessage(string name)
        {
            return "用户名必须包含abc";
        }
    }
}

第一个验证属性要求宿主的内容不能为空,第二个验证属性要求内容必须含有abc这个字符串。

然后我们又要用到Behavior了。当绑定的内容校验出异常后,它会一起冒泡,只到Window。这时候,Window的Behavior接收到异常,做出相应的处理。

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Interactivity;

namespace LoginDemo.ViewModel.Common
{
    /// <summary>
    /// 验证异常行为
    /// </summary>
    public class ValidationExceptionBehavior : Behavior<FrameworkElement>
    {
        /// <summary>
        /// 记录异常的数量
        /// </summary>
        /// <remarks>在一个页面里面,所有控件的验证错误信息都会传到这个类上,每个控制需不需要显示验证错误,需要分别记录</remarks>
        private Dictionary<UIElement, int> ExceptionCount;
        /// <summary>
        /// 缓存页面的提示装饰器
        /// </summary>
        private Dictionary<UIElement, NotifyAdorner> AdornerDict;

        protected override void OnAttached()
        {
            ExceptionCount = new Dictionary<UIElement, int>();
            AdornerDict = new Dictionary<UIElement, NotifyAdorner>();

            this.AssociatedObject.AddHandler(Validation.ErrorEvent, new EventHandler<ValidationErrorEventArgs>(OnValidationError));
        }

        /// <summary>
        /// 当验证错误信息改变时,首先调用此函数
        /// </summary>
        private void OnValidationError(object sender, ValidationErrorEventArgs e)
        {
            try
            {
                var handler = GetValidationExceptionHandler();//插入<c:ValidationExceptionBehavior></c:ValidationExceptionBehavior>此语句的窗口的DataContext,也就是ViewModel
                var element = e.OriginalSource as UIElement;//错误信息发生改变的控件
                if (handler == null || element == null)
                {
                    return;
                }

                if (e.Action == ValidationErrorEventAction.Added)
                {
                    if (ExceptionCount.ContainsKey(element))
                    {
                        ExceptionCount[element]++;
                    }
                    else
                    {
                        ExceptionCount.Add(element, 1);
                    }
                }
                else if (e.Action == ValidationErrorEventAction.Removed)
                {
                    if (ExceptionCount.ContainsKey(element))
                    {
                        ExceptionCount[element]--;
                    }
                    else
                    {
                        ExceptionCount.Add(element, -1);
                    }
                }

                if (ExceptionCount[element] <= 0)
                {
                    HideAdorner(element);
                }
                else
                {
                    ShowAdorner(element, e.Error.ErrorContent.ToString());
                }

                int TotalExceptionCount = 0;
                foreach (KeyValuePair<UIElement, int> kvp in ExceptionCount)
                {
                    TotalExceptionCount += kvp.Value;
                }

                handler.IsValid = (TotalExceptionCount <= 0);//ViewModel里面的IsValid
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// <summary>
        /// 获得行为所在窗口的DataContext
        /// </summary>
        private NotificationObject GetValidationExceptionHandler()
        {
            if (this.AssociatedObject.DataContext is NotificationObject)
            {
                var handler = this.AssociatedObject.DataContext as NotificationObject;

                return handler;
            }

            return null;
        }

        /// <summary>
        /// 显示错误信息提示
        /// </summary>
        private void ShowAdorner(UIElement element, string errorMessage)
        {
            if (AdornerDict.ContainsKey(element))
            {
                AdornerDict[element].ChangeToolTip(errorMessage);
            }
            else
            {
                var adornerLayer = AdornerLayer.GetAdornerLayer(element);
                NotifyAdorner adorner = new NotifyAdorner(element, errorMessage);
                adornerLayer.Add(adorner);
                AdornerDict.Add(element, adorner);
            }
        }

        /// <summary>
        /// 隐藏错误信息提示
        /// </summary>
        private void HideAdorner(UIElement element)
        {
            if (AdornerDict.ContainsKey(element))
            {
                var adornerLayer = AdornerLayer.GetAdornerLayer(element);
                adornerLayer.Remove(AdornerDict[element]);
                AdornerDict.Remove(element);
            }
        }
    }
}

这里异常的处理方式是显示我们最开始戴图的叹号图形。这个图形由NotifyAdnoner完成显示:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace LoginDemo.ViewModel.Common
{
    /// <summary>
    /// 带有感叹号的提示图形
    /// </summary>
    public class NotifyAdorner : Adorner
    {
        private VisualCollection _visuals;
        private Canvas _canvas;
        private Image _image;
        private TextBlock _toolTip;

        public NotifyAdorner(UIElement adornedElement, string errorMessage) : base(adornedElement)
        {
            _visuals = new VisualCollection(this);

            _image = new Image()
            {
                Width = 16,
                Height = 16,
                Source = new BitmapImage(new Uri("/warning.png", UriKind.RelativeOrAbsolute))
            };

            _toolTip = new TextBlock() { Text = errorMessage };
            _image.ToolTip = _toolTip;

            _canvas = new Canvas();
            _canvas.Children.Add(_image);
            _visuals.Add(_canvas);
        }

        protected override int VisualChildrenCount
        {
            get
            {
                return _visuals.Count;
            }
        }

        protected override Visual GetVisualChild(int index)
        {
            return _visuals[index];
        }

        public void ChangeToolTip(string errorMessage)
        {
            _toolTip.Text = errorMessage;
        }

        protected override Size MeasureOverride(Size constraint)
        {
            return base.MeasureOverride(constraint);
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            _canvas.Arrange(new Rect(finalSize));
            _image.Margin = new Thickness(finalSize.Width + 3, 0, 0, 0);

            return base.ArrangeOverride(finalSize);
        }
    }
}

我们的ViewModel也要对数据验证做出支持。由于我们先前让ViewModel继承了NotificationObject,它并不是一个接口,我们不能继承两个类。所以,我们在NotificationObject里面加入验证有内容(虽然这样不太好)。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace LoginDemo.ViewModel.Common
{
    public abstract class NotificationObject : INotifyPropertyChanged, IDataErrorInfo
    {
        #region 属性修改通知

        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// 发起通知
        /// </summary>
        /// <param name="propertyName">属性名</param>
        public void RaisePropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        #region 数据验证

        public string Error
        {
            get { return ""; }
        }

        public string this[string columnName]
        {
            get
            {
                var vc = new ValidationContext(this, null, null);
                vc.MemberName = columnName;
                var res = new List<ValidationResult>();
                var result = Validator.TryValidateProperty(this.GetType().GetProperty(columnName).GetValue(this, null), vc, res);
                if (res.Count > 0)
                {
                    return string.Join(Environment.NewLine, res.Select(r => r.ErrorMessage).ToArray());
                }
                return string.Empty;
            }
        }

        /// <summary>
        /// 页面中是否所有控制数据验证正确
        /// </summary>
        public virtual bool IsValid { get; set; }

        #endregion
    }
}

至此,准备就绪。我们修改ViewModel里面的UserName和Password属性:

/// <summary>
/// 用户名
/// </summary>
[NotEmptyCheck]
[UserNameExists]
public string UserName
{
    get
    {
        return obj.UserName;
    }
    set
    {
        obj.UserName = value;
        this.RaisePropertyChanged("UserName");
    }
}

/// <summary>
/// 密码
/// </summary>
[NotEmptyCheck]
public string Password
{
    get
    {
        return obj.Password;
    }
    set
    {
        obj.Password = value;
        this.RaisePropertyChanged("Password");
    }
}

没错,就是加了头上中括号的内容。这样的话,UserName就被要求非空和包含abc,而密码则被要求非空。由于我们在NotificationObject里加入了IsValid虚属性,还必须实现一下:

/// <summary>
/// 数据填写正确
/// </summary>
public override bool IsValid
{
    get
    {
        return obj.IsValid;
    }
    set
    {
        if (value == obj.IsValid)
        {
            return;
        }
        obj.IsValid = value;
        this.RaisePropertyChanged("IsValid");
    }
}

这个IsValid的设置是在ValidationExceptionBehavior里完成的。登录按钮只要绑定这个属性,就能在出现验证异常时,变成禁用。

我们修改XAML文件的用户名、密码和登录按钮:

<TextBox Grid.Row="0" Grid.Column="1" Margin="5" Text="{Binding UserName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"/>

<PasswordBox Grid.Row="1" Grid.Column="1" Margin="5" c:PasswordBoxHelper.Password="{Binding Password,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,ValidatesOnExceptions=True,ValidatesOnDataErrors=True,NotifyOnValidationError=True}">
    <i:Interaction.Behaviors>
        <c:PasswordBoxBehavior/>
    </i:Interaction.Behaviors>
</PasswordBox>

<Button Grid.Row="3" Grid.ColumnSpan="2" Content="登录" Width="200" Height="30" IsEnabled="{Binding IsValid}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <c:EventCommand Command="{Binding LoginClick}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

窗口刚打开的时候是这样的,登录按钮被禁用:

技术图片

当数据都输入正确,登录按钮被启用:

技术图片

至此,登录窗口的所有功能就介绍完了。也恭喜你,你已经能熟练地使用MVVM模式了。

以上是关于SpringSecurity从入门到精通:从数据库查询权限信息&自定义失败处理的主要内容,如果未能解决你的问题,请参考以下文章

WPF MVVM从入门到精通3:数据绑定

WPF MVVM从入门到精通8:数据验证

MySQL从入门到精通(九) MySQL锁,各种锁

上新 | 《Oracle数据库管理从入门到精通》《Oracle PL/SQL从入门到精通 》

DDD(领域驱动设计)从入门到精通

详细讲解Quartz如何从入门到精通