欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

spring security入门demo

程序员文章站 2023-11-11 14:24:16
一、前言 因项目需要引入spring security权限框架,而之前也没接触过这个一门,于是就花了点时间弄了个小demo出来,说实话,刚开始接触这个确实有点懵,看网上资料写的权限大都是静态,即就是在配置文件或代码里面写定角色,不能动态更改,个人感觉这样实际场景应该应用的不多,于是就进一步研究,整理 ......

一、前言

  因项目需要引入spring security权限框架,而之前也没接触过这个一门,于是就花了点时间弄了个小demo出来,说实话,刚开始接触这个确实有点懵,看网上资料写的权限大都是静态,即就是在配置文件或代码里面写定角色,不能动态更改,个人感觉这样实际场景应该应用的不多,于是就进一步研究,整理出了一个可以动态管理个*限角色demo,其中可能有很多不足或之处,还望指正。本文通过spring boot集成spring security,处理方式没有使用xml文件格式,而是用了注解。

 二、表结构

接触过权限这块的,大都应该知道,最核心的有三张表(当然,如果牵涉业务复杂,可能不止)。

一、用户表

二、角色表

三、菜单表(即权限表)

剩余还有两张多对多的表。即用户与角色,角色与菜单。如下图

spring security入门demo

三、spring security入口

由于本文只是着重说spring security,关于spring boot一块内容会直接带过。如spring boot启动类配置等。

首先会自定义一个类去实现websecurityconfigureradapter类。重写其中几个方法,代码如下

 1 @configuration
 2 public class websecurityconfig extends websecurityconfigureradapter {
 3 
 4     @autowired
 5     @qualifier(value = "userdetailserviceimpl")
 6     private userdetailsservice userdetailsservice;
 7 
 8     @autowired
 9     private loginsuccessauthenticationhandler successauthenticationhandler;
10 
11     @autowired
12     private loginfailureauthenticationhandler failureauthenticationhandler;
13 
14     @autowired
15     private authenticationaccessdeniedhandler accessdeniedhandler;
16 
17     @autowired
18     private urlaccessdecisionmanager decisionmanager;
19 
20     @autowired
21     private urlpathfilterinvocationsecuritymetadatasource urlpathfilterinvocationsecuritymetadatasource;
22 
23     @autowired
24     private authenticationprovider authenticationprovider;
25 
26     @autowired
27     private passwordencoder passwordencoder;
28 
29     @override
30     protected void configure(authenticationmanagerbuilder auth) throws exception {
31         auth.userdetailsservice(userdetailsservice).passwordencoder(passwordencoder);
32         auth.authenticationprovider(authenticationprovider);
33     }
34 
35     @override
36     public void configure(websecurity web) {
37         web.ignoring().antmatchers("/index.html","/favicon.ico");
38     }
39 
40     @override
41     protected void configure(httpsecurity http) throws exception {
42         http.csrf().disable()
43                 .authorizerequests()
44                 .withobjectpostprocessor(new objectpostprocessor<filtersecurityinterceptor>() {
45                     @override
46                     public <o extends filtersecurityinterceptor> o postprocess(o o) {
47                         o.setaccessdecisionmanager(decisionmanager);
48                         o.setsecuritymetadatasource(urlpathfilterinvocationsecuritymetadatasource);
49                         return o;
50                     }
51                 })
52 
53                 .anyrequest()
54                 .authenticated()// 其他 url 需要身份认证
55 
56                 .and()
57                 .formlogin()  //开启登录,如果不指定登录路径(即输入用户名和密码表单提交的路径),则会默认为spring securtiy的内部定义的路径
58                 .successhandler(successauthenticationhandler)
59                 .failurehandler(failureauthenticationhandler)// 遇到用户名或密码不正确/用户被锁定等情况异常,会交给此handler处理
60                 .permitall()
61 
62                 .and()
63                 .logout()
64                 .logouturl("/logout")//退出操作,其实也有一个handler,如果没其他业务逻辑,可以默认为spring security的handler
65                 .permitall()
66                 .and()
67                 .exceptionhandling().accessdeniedhandler(accessdeniedhandler);
68     }

在这里会介绍以下几个类作用

一、userdetailsservice
二、authenticationprovider
三、authenticationaccessdeniedhandler
四、urlaccessdecisionmanager
五、urlpathfilterinvocationsecuritymetadatasource
至于loginsuccessauthenticationhandler、loginfailureauthenticationhandler就是用来处理登录成功和登录失败情况,这里不做介绍

3.1、userdetailservice的作用

这个一个接口,通常我们需要去实现它,作用主要是用来我们和数据库做交互用的。简单来说,就是用户名传过来,这个类负责校验用户名是否存在等业务逻辑。

 1 @component
 2 public class userdetailserviceimpl implements userdetailsservice {
 3 
 4     @autowired
 5     private sysuserdao userdao;
 6 
 7     @autowired
 8     private passwordencoder passwordencoder;
 9 
10 
11     @override
12     public userdetails loaduserbyusername(string s) throws usernamenotfoundexception {
13         sysuser sysuser = userdao.findbyusername(s);
14         if (sysuser == null){
15             throw new usernamenotfoundexception("用户不存在");
16         }
17         string pwd = passwordencoder.encode(sysuser.getpassword());
18         system.out.println(pwd);
19         return new user(sysuser.getusername(),pwd,getroles(sysuser.getroles()));
20     }
21 
22     private collection<grantedauthority> getroles(list<sysrole> roles){
23         list<grantedauthority> list = new arraylist<>();
24         for (sysrole role : roles){
25             simplegrantedauthority grantedauthority = new simplegrantedauthority(role.getrolename());
26             list.add(grantedauthority);
27         }
28         return list;
29     }
30 }

 

代码比较简单,值得注意的是sercurity里的user对象,它的一个构造函数有是哪个参数值,第一个和第二个是用户名和密码,密码作用就是后面用来校验前端传过来的密码正确性。稍后会讲到。至于第三个参数就是当前用户所拥有的角色,作用就是在当前端请求一个接口的时候,会判断这个接口所拥有的权限和该用户所有的权限有重合,简单来说就是该用户是否拥有该接口权限。这里也就实现了一个角色可以动态修改的功能。因其实从数据库查询出来。

3.2、authenticationprovider

它也是一个接口,它的作用是用来校验用户密码等功能,当然如短信验证或要第三方验证,也可以实现这个接口,在本文中是用密码校验。前面也说到userdetailservice会传一个用户的基本信息。它的主要作用就是为该接口服务的。

 1 @component
 2 public class loginauthenticationprovider implements authenticationprovider {
 3 
 4     @autowired
 5     private userdetailsservice userdetailsservice;
 6 
 7     @override
 8     public authentication authenticate(authentication authentication) throws authenticationexception {
 9         // 获取表单用户名
10         string username = (string) authentication.getprincipal();
11         // 获取表单用户填写的密码
12         string password = (string) authentication.getcredentials();
13 
14         userdetails userdetails = userdetailsservice.loaduserbyusername(username);
15 
16         string password1 = userdetails.getpassword();
17         if (!objects.equals(password,password1)){
18             throw new badcredentialsexception("用户名或密码不正确");
19         }
20 
21         return new usernamepasswordauthenticationtoken(username,password,userdetails.getauthorities());
22     }
23 
24     @override
25     public boolean supports(class<?> aclass) {
26         return true;
27     }
28 }

 

值得注意的是如果验证通过会返回一个usernamepasswordauthenticationtoken对象,它的作用就是标志着此用户已通过登录验证,如果没通过,则spring security会捕捉如代码18行的异常,然后再包装一个匿名的token,即anonymousauthenticationtoken,此token即代表用户未登录。两个接口主要服务于用户登录这块。接下来的三个是服务于权限校验。即接口验证

3.3、urlpathfilterinvocationsecuritymetadatasource

 它的作用是用来处理当前用户是否拥有此接口的权限。

 1 @component
 2 public class urlpathfilterinvocationsecuritymetadatasource implements filterinvocationsecuritymetadatasource {
 3 
 4 
 5     @autowired
 6     private sysmenudao sysmenudao;
 7 
 8     private antpathmatcher antpathmatcher = new antpathmatcher();
 9 
10     @override
11     public collection<configattribute> getattributes(object object) throws illegalargumentexception {
12         filterinvocation filterinvocation = (filterinvocation) object;
13         string requesturl = filterinvocation.getrequesturl();
14         // 因为菜单一般随着开发完成,变动不大,此处可以使用缓存,这里为了演示,就直接查库,菜单对应角色需要动态情缓存,如变更菜单和角色关系,需清除缓存
15         list<sysmenu> all = sysmenudao.findall();
16         for (sysmenu menu : all) {
17             if (menu.getroles().size() != 0 && antpathmatcher.match(menu.geturlpath(), requesturl)) {
18                 list<sysrole> roles = menu.getroles();
19                 int size = roles.size();
20                 string[] values = new string[size];
21                 for (int i = 0; i < size; i++) {
22                     values[i] = roles.get(i).getrolename();
23                 }
24                 return securityconfig.createlist(values);
25             }
26         }
27         return securityconfig.createlist("role_login");
28     }
29 
30     @override
31     public collection<configattribute> getallconfigattributes() {
32         return null;
33     }
34 
35     @override
36     public boolean supports(class<?> clazz) {
37         return true;
38     }
39 }

 

从代码就可以看出16行的for循环就是获取当前请求接口锁需要的权限,这里使用spring security的路径匹配类。如果该接口·没有权限,这里返回一个标志如role_login,当然如果需要其他标志可以自行定义,这里为了简便,就用了这个。

3.4、urlaccessdecisionmanager

这个类就是最终的决策类。从3.1到3.2,大家都清楚,已有的信息,用户所有的权限这个已经获取到了,3.3可知当前请求接口的权限也已经获取到了,剩下的肯定就是比较两这个权限集合有没有交集,如果有则表明当前用户拥有此接口的权限。

 1 @component
 2 public class urlaccessdecisionmanager implements accessdecisionmanager {
 3 
 4     /**
 5      *
 6      * @param authentication 当前用户信息,和当前用户的拥有权限信息,即来自于userdetailservice里的
 7      * @param object 即filterinvocation对象,可以获取httpservletrequest请求对象
 8      * @param configattributes  本次访问所需要的权限
 9      * @throws accessdeniedexception
10      * @throws insufficientauthenticationexception
11      */
12     @override
13     public void decide(authentication authentication, object object, collection<configattribute> configattributes) throws accessdeniedexception, insufficientauthenticationexception {
14         iterator<configattribute> iterator = configattributes.iterator();
15         while (iterator.hasnext()) {
16             configattribute ca = iterator.next();
17             //当前请求需要的权限
18             string needrole = ca.getattribute();
19             if ("role_login".equals(needrole)) {
20                 // 即匿名用户/未登录,如果用户登录成功。那么authententication就是前面提到的usernamepasswordauthententicationtoken类
21                 if (authentication instanceof anonymousauthenticationtoken) {
22                     throw new badcredentialsexception("未登录");
23                 } else {// 登录但不具有此路径权限,即前面3.3提到的role_login,接口没有角色对应,主要用户已经登录成功
24                     break;
25                 }
26             }
27             //当前用户所具有的权限
28             collection<? extends grantedauthority> authorities = authentication.getauthorities();
29             for (grantedauthority authority : authorities) {
30                 if (authority.getauthority().equals(needrole)) {
31                     return;
32                 }
33             }
34         }
35         throw new accessdeniedexception("权限不足!");
36     }
37 
38     @override
39     public boolean supports(configattribute attribute) {
40         return true;
41     }
42 
43     @override
44     public boolean supports(class<?> clazz) {
45         return true;
46     }
47 }

3.5、authenticationaccessdeniedhandler

这个类就是用来接收上面抛出的accessdeniedexception异常,

 1 @component
 2 public class authenticationaccessdeniedhandler implements accessdeniedhandler {
 3 
 4 
 5     @override
 6     public void handle(httpservletrequest httpservletrequest, httpservletresponse httpservletresponse, accessdeniedexception e) throws ioexception, servletexception {
 7         httpservletresponse.setstatus(httpservletresponse.sc_forbidden);
 8         httpservletresponse.setcontenttype("application/json;charset=utf-8");
 9         printwriter writer = httpservletresponse.getwriter();
10 
11         writer.print("权限不足");
12         writer.flush();
13     }
14 }

 

至于哪种异常由哪个类处理,如果了解源码的都知道spring security有一个异常处理过滤器,名字为exceptiontranslationfilter,要想进一步了解的,可自行看源码,这里提供一个个人认为写的挺好的博文,链接地址,这里不多说废话。

相信大家看完以上文章,对spring security应该有一个大致的了解,,这里附上一个spring security请求经过的过滤器filter,

spring security入门demo

执行顺序从上到下。要想研究一波,大家可以先从delegatingfilterproxy类及它的父类开始入手,一步一步debug下去,相信会有收获的。关于websecurityconfig 的配置情况,这里也不多说,网上文章也挺多的。在这里说下当初遇到的一个比较坑的坑

四、遇到的坑

当时场景是这样的,因为项目采用的是前后端分离模式开发的,后端写完代码需要部署到测试服务器,供前端使用,采用的域名是https模式,使用了nginx代码模式,部署上去后。因为登录失败后,spring security会请求到你指定的一个路径,但此时问题出现了,代码部署上去了,测试了一个用户名和密码不正确的情况,结果发现跳转后的host由https变成了http,例子:本来是请求https://abc.com/dologin路径,但是变成了htttp://abc.com/dologin。这肯定是访问不了,当时就有点懵了,后面经过分析发现,更改nginx配置可以达到指定效果,在指定的location加入proxy_set_header x-forwarded-proto https,但是这样局限性也有,这样做只能使用https进行访问,所以就没采用,后来就直接百度,百度了的结果大都是更改spring mvc 内部视图解析器配置,如下面

 

1 <bean id="viewresolver" class="org.springframework.web.servlet.view.internalresourceviewresolver">
2   <property name="viewclass" value="org.springframework.web.servlet.view.jstlview" />
3   <property name="prefix" value="/web-inf/" />
4   <property name="suffix" value=".jsp" /> 
5    <!-- 重点是下面配置,将其改为false -->
6   <property name="redirecthttp10compatible" value="false" />
7 </bean>

 

 

 

不过redirect也提醒了我,这个情况由https 变成http 应该就是redirect搞的鬼。那如果将spring security内部由redirect改成forward呢,那情况又会怎样,紧接着,又去看其源码,最后发现这样一个类loginurlauthenticationentrypoint负责spring security的重定向和转发情况,在其commence方法内进行操作,最后那肯定得试试,最后将该类的useforward属性设置成了true,然后就完美解决。

 

 

 --------------------------------------------------------------------------------------------------------------------------------------------------分界线--------------------------------------------------------------------------------------

以上就是全部内容,若有不足之处,还望指正,另外附上本文代码地址供大家参考