好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

spring boot 集成 shiro 自定义密码验证 自定义freemarker标签根据权限渲染

项目里一直用的是 spring-security ,不得不说,spring-security 真是东西太多了,学习难度太大(可能我比较菜),这篇博客来总结一下折腾 shiro 的成果,分享给大家,强烈推荐shiro,真心简单 : )

引入依赖

?

1

2

3

4

5

<dependency>

  <groupid>org.apache.shiro</groupid>

  <artifactid>shiro-spring</artifactid>

  <version> 1.4 . 0 </version>

</dependency>

用户,角色,权限

就是经典的rbac权限系统,下面简单给一下实体类字段

adminuser.java

?

1

2

3

4

5

6

7

8

9

public class adminuser implements serializable {

 

  private static final long serialversionuid = 8264158018518861440l;

  private integer id;

  private string username;

  private string password;

  private integer roleid;

  // getter setter...

}

role.java

?

1

2

3

4

5

6

public class role implements serializable {

  private static final long serialversionuid = 7824693669858106664l;

  private integer id;

  private string name;

  // getter setter...

}

permission.java

?

1

2

3

4

5

6

7

8

9

public class permission implements serializable {

  private static final long serialversionuid = -2694960432845360318l;

  private integer id;

  private string name;

  private string value;

  // 权限的父节点的id

  private integer pid;

  // getter setter...

}

自定义realm

这货就是查询用户的信息然后放在shiro的个人用户对象的缓存里,shiro自己有一个session的对象(不是servlet里的session)作用就是后面用户发起请求的时候拿来判断有没有权限

另一个作用是查询一下用户的信息,将用户名,密码组装成一个 authenticationinfo 用于后面密码校验的

具体代码如下

myshirorealm.java

 

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

@component

public class myshirorealm extends authorizingrealm {

  private logger log = loggerfactory.getlogger(myshirorealm. class );

  @autowired

  private adminuserservice adminuserservice;

  @autowired

  private roleservice roleservice;

  @autowired

  private permissionservice permissionservice;

  // 用户权限配置

  @override

  protected authorizationinfo dogetauthorizationinfo(principalcollection principals) {

  //访问@requirepermission注解的url时触发

  simpleauthorizationinfo simpleauthorizationinfo = new simpleauthorizationinfo();

  adminuser adminuser = adminuserservice.selectbyusername(principals.tostring());

  //获得用户的角色,及权限进行绑定

  role role = roleservice.selectbyid(adminuser.getroleid());

  // 其实这里也可以不要权限那个类了,直接用角色这个类来做鉴权,

  // 不过角色包含很多的权限,已经算是大家约定的了,所以下面还是查询权限然后放在authorizationinfo里

  simpleauthorizationinfo.addrole(role.getname());

  // 查询权限

  list<permission> permissions = permissionservice.selectbyroleid(adminuser.getroleid());

  // 将权限具体值取出来组装成一个权限string的集合

  list<string> permissionvalues = permissions.stream().map(permission::getvalue).collect(collectors.tolist());

  // 将权限的string集合添加进authorizationinfo里,后面请求鉴权有用

  simpleauthorizationinfo.addstringpermissions(permissionvalues);

  return simpleauthorizationinfo;

  }

  // 组装用户信息

  @override

  protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception {

  string username = (string) token.getprincipal();

  log.info( "用户:{} 正在登录..." , username);

  adminuser adminuser = adminuserservice.selectbyusername(username);

  // 如果用户不存在,则抛出未知用户的异常

  if (adminuser == null ) throw new unknownaccountexception();

  return new simpleauthenticationinfo(username, adminuser.getpassword(), getname());

  }

}

实现密码校验

shiro内置了几个密码校验的类,有 md5credentialsmatcher sha1credentialsmatcher , 不过从1.1版本开始,都开始使用 hashedcredentialsmatcher 这个类了,通过配置加密规则来校验

它们都实现了一个接口 credentialsmatcher 我这里也实现这个接口,实现一个自己的密码校验

说明一下,我这里用的加密方式是spring-security里的 bcryptpasswordencoder 作的加密,之所以用它,是因为同一个密码被这货加密后,密文都不一样,下面是具体代码

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

public class mycredentialsmatcher implements credentialsmatcher {

 

  @override

  public boolean docredentialsmatch(authenticationtoken token, authenticationinfo info) {

  // 大坑!!!!!!!!!!!!!!!!!!!

  // 明明token跟info两个对象的里的credentials类型都是object,断点看到的类型都是 char[]

  // 但是!!!!! token里转成string要先强转成 char[]

  // 而info里取credentials就可以直接使用 string.valueof() 转成string

  // 醉了。。

  string rawpassword = string.valueof(( char []) token.getcredentials());

  string encodedpassword = string.valueof(info.getcredentials());

  return new bcryptpasswordencoder().matches(rawpassword, encodedpassword);

  }

}

配置shiro

因为项目是spring-boot开发的,shiro就用java代码配置,不用xml配置, 具体配置如下

 

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

@configuration

public class shiroconfig {

  private logger log = loggerfactory.getlogger(shiroconfig. class );

  @autowired

  private myshirorealm myshirorealm;

  @bean

  public shirofilterfactorybean shirofilter(securitymanager securitymanager) {

  log.info( "开始配置shirofilter..." );

  shirofilterfactorybean shirofilterfactorybean = new shirofilterfactorybean();

  shirofilterfactorybean.setsecuritymanager(securitymanager);

  //拦截器.

  map<string,string> map = new hashmap<>();

  // 配置不会被拦截的链接 顺序判断 相关静态资源

  map.put( "/static/**" , "anon" );

  //配置退出 过滤器,其中的具体的退出代码shiro已经替我们实现了

  map.put( "/admin/logout" , "logout" );

  //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;

  //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->

  map.put( "/admin/**" , "authc" );

  // 如果不设置默认会自动寻找web工程根目录下的"/login.jsp"页面

  shirofilterfactorybean.setloginurl( "/adminlogin" );

  // 登录成功后要跳转的链接

  shirofilterfactorybean.setsuccessurl( "/admin/index" );

  //未授权界面;

  shirofilterfactorybean.setunauthorizedurl( "/error" );

  shirofilterfactorybean.setfilterchaindefinitionmap(map);

  return shirofilterfactorybean;

  }

  // 配置加密方式

  // 配置了一下,这货就是验证不过,,改成手动验证算了,以后换加密方式也方便

  @bean

  public mycredentialsmatcher mycredentialsmatcher() {

  return new mycredentialsmatcher();

  }

  // 安全管理器配置

  @bean

  public securitymanager securitymanager() {

  defaultwebsecuritymanager securitymanager = new defaultwebsecuritymanager();

  myshirorealm.setcredentialsmatcher(mycredentialsmatcher());

  securitymanager.setrealm(myshirorealm);

  return securitymanager;

  }

}

登录

都配置好了,就可以发起登录请求做测试了,一个简单的表单即可,写在controller里就行

 

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

@postmapping ( "/adminlogin" )

public string adminlogin(string username, string password,

        @requestparam (defaultvalue = "0" ) boolean rememberme,

        redirectattributes redirectattributes) {

  try {

  // 添加用户认证信息

  subject subject = securityutils.getsubject();

  if (!subject.isauthenticated()) {

   usernamepasswordtoken token = new usernamepasswordtoken(username, password, rememberme);

   //进行验证,这里可以捕获异常,然后返回对应信息

   subject.login(token);

  }

  } catch (authenticationexception e) {

  // e.printstacktrace();

  log.error(e.getmessage());

  redirectattributes.addflashattribute( "error" , "用户名或密码错误" );

  redirectattributes.addflashattribute( "username" , username);

  return redirect( "/adminlogin" );

  }

  return redirect( "/admin/index" );

}

从上面代码可以看出,记住我功能也直接都实现好了,只需要在组装 usernamepasswordtoken 的时候,将记住我字段传进去就可以了,值是 true, false, 如果是true,登录成功后,shiro会在本地写一个cookie

调用 subject.login(token); 方法后,它会去鉴权,期间会产生各种各样的异常,有以下几种,可以通过捕捉不同的异常然后提示页面不同的错误信息,相当的方便呀,有木有

accountexception 帐户异常 concurrentaccessexception 这个好像是并发异常 credentialsexception 密码校验异常 disabledaccountexception 帐户被禁异常 excessiveattemptsexception 尝试登录次数过多异常 expiredcredentialsexception 认证信息过期异常 incorrectcredentialsexception 密码不正确异常 lockedaccountexception 帐户被锁定异常 unknownaccountexception 未知帐户异常 unsupportedtokenexception login(authenticationtoken) 这个方法只能接收 authenticationtoken 类的对象,如果传的是其它的类,就抛这个异常

上面这么多异常,shiro在处理登录的逻辑时,会自动的发出一些异常,当然你也可以手动去处理登录流程,然后根据不同的问题抛出不同的异常,手动处理的地方就在自己写的 myshirorealm 里的 dogetauthenticationinfo() 方法里,我在上面代码里只处理了一个帐户不存在时抛出了一个 unknownaccountexception 的异常,其实还可以加更多其它的异常,这个要看个人系统的需求来定了

到这里已经可以正常的实现登录了,下面来说一些其它相关的功能的实现

自定 freemarker 标签

开发项目肯定要用到页面模板,我这里用的是 freemarker ,一个用户登录后,页面可能要根据用户的不同权限渲染不同的菜单,github上有个开源的库,也是可以用的,不过我觉得那个太麻烦了,就自己实现了一个,几行代码就能搞定

shirotag.java

 

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

@component

public class shirotag {

  // 判断当前用户是否已经登录认证过

  public boolean isauthenticated(){

  return securityutils.getsubject().isauthenticated();

  }

  // 获取当前用户的用户名

  public string getprincipal() {

  return (string) securityutils.getsubject().getprincipal();

  }

  // 判断用户是否有 xx 角色

  public boolean hasrole(string name) {

  return securityutils.getsubject().hasrole(name);

  }

  // 判断用户是否有 xx 权限

  public boolean haspermission(string name) {

  return !stringutils.isempty(name) && securityutils.getsubject().ispermitted(name);

  }

}

将这个类注册到freemarker的全局变量里

freemarkerconfig.java

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@configuration

public class freemarkerconfig {

  private logger log = loggerfactory.getlogger(freemarkerconfig. class );

  @autowired

  private shirotag shirotag;

  @postconstruct

  public void setsharedvariable() throws templatemodelexception {

  //注入全局配置到freemarker

  log.info( "开始配置freemarker全局变量..." );

  // shiro鉴权

  configuration.setsharedvariable( "sec" , shirotag);

  log.info( "freemarker自定义标签配置完成!" );

  }

}

有了这些配置后,就可以在页面里使用了,具体用法如下

?

1

2

3

4

5

6

7

8

<# if sec.haspermission( "topic:list" )>

  <li <# if page_tab== 'topic' > class = "active" </# if >>

  <a href= "/admin/topic/list" rel= "external nofollow" >

   <i class = "fa fa-list" ></i>

   <span>话题列表</span>

  </a>

  </li>

</# if >

加上这个后,在渲染页面的时候,就会根据当前用户是否有查看话题列表的权限,然后来渲染这个菜单

注解权限

有了上面freemarker标签判断是否有权限来渲染页面,这样做只能防君子,不能防小人,如果一个人知道后台的某个访问链接,但这个链接它是没有权限访问的,那他只要手动输入这个链接就还是可以访问的,所以这里还要在controller层加一套防御,具体配置如下

在shiroconfig里加上两个bean

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

//加入注解的使用,不加入这个注解不生效

@bean

public authorizationattributesourceadvisor authorizationattributesourceadvisor(defaultwebsecuritymanager securitymanager) {

  authorizationattributesourceadvisor authorizationattributesourceadvisor = new authorizationattributesourceadvisor();

  authorizationattributesourceadvisor.setsecuritymanager(securitymanager);

  return authorizationattributesourceadvisor;

}

@bean

@conditionalonmissingbean

public defaultadvisorautoproxycreator defaultadvisorautoproxycreator() {

  defaultadvisorautoproxycreator defaultaap = new defaultadvisorautoproxycreator();

  defaultaap.setproxytargetclass( true );

  return defaultaap;

}

有了这两个bean就可以用shiro的注解鉴权了,用法如下 @requirespermissions("topic:list")

 

?

1

2

3

4

5

6

7

8

9

10

11

@controller

@requestmapping ( "/admin/topic" )

public class topicadmincontroller extends baseadmincontroller {

 

  @requirespermissions ( "topic:list" )

  @getmapping ( "/list" )

  public string list() {

  // todo

  return "admin/topic/list" ;

  }

}

shiro除了 @requirespermissions 注解外,还有其它几个鉴权的注解

@requirespermissions @requiresroles @requiresuser @requiresguest @requiresauthentication

一般 @requirespermissions 就够用了

总结

spring-boot 集成 shiro 到这就结束了,是不是网上能找到的教程里最全的!相比 spring-security 要简单太多了,强烈推荐

原文链接:https://tomoya92.github.io/2018/12/05/spring-boot-shiro/

查看更多关于spring boot 集成 shiro 自定义密码验证 自定义freemarker标签根据权限渲染的详细内容...

  阅读:13次