SpringBoot+Shiro实现登陆拦截功能

      上一章讲到使用自定义的方式来实现用户登录的功能,这章采用shiro来实现用户登陆拦截的功能。

      首先介绍下Shiro:Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理,以下是shiro的整体的框架:

SpringBoot+Shiro实现登陆拦截功能

Subject: 即"用户",外部应用都是和Subject进行交互的subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subjectshiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权(Subject相当于SecurityManager的门面)。

SecurityManager: 安全管理器它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。此外SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口

Authenticator:是一个执行对用户的身份验证(登录)的组件。通过与一个或多个Realm 协调来存储相关的用户/帐户信息。Realm中找到对应的数据,明确是哪一个登陆人。如果存在多个realm,则接口AuthenticationStrategy(策略)会确定什么样算是登录成功(例如,如果一个Realm成功,而其他的均失败,是否登录成功?)。它是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。

Authorizer:即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。就是用来判断是否有权限,授权,本质就是访问控制,控制哪些URL可以访问.

Realm:即领域,相当于datasource数据源securityManager进行安全认证需要通过Realm获取用户权限数据,通常一个数据源配置一个realm.s比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。

注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码

SessionDAO:即会话dao是对session会话操作的一套接口SessionDao代替sessionManager来代替对session进行增删改查,允许用户使用任何类型的数据源来存储session数据,也可以将数据引入到session框架来。比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。

CacheManager:缓存管理,用于管理其他shiro组件中维护和创建的cache实例,维护这些cache实例的生命周期,缓存那些从后台获取的用于用户权限,验证的数据,将它们存储在缓存,这样可以提高性能顺序:先从缓存中查找,再从后台其他接口从其它数据源中进行查找,可以用其他现代的企业级数据源来代替默认的数据源来提高性能

Cryptography:密码管理shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

我们可以把和shiro的交互用下图来表示:

SpringBoot+Shiro实现登陆拦截功能

这个是Shiro身份认证的流程图:

SpringBoot+Shiro实现登陆拦截功能

(注:这个图片是从其他博客拷贝过来的,)

这是Shiro的认证流程:

SpringBoot+Shiro实现登陆拦截功能

流程如下

1、首先调用Subject.isPermitted*/hasRole*接口,其会委托给SecurityManager,而SecurityManager接着会委托给Authorizer

2Authorizer是真正的授权者,如果我们调用如isPermitted(user:view),其首先会通过PermissionResolver把字符串转换成相应的Permission实例

3、在进行授权之前,其会调用相应Realm获取Subject相应的角色/权限用于匹配传入的角色/权限

4Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted*/hasRole*会返回true,否则返回false表示授权失败。

 

ModularRealmAuthorizer进行Realm匹配流程

1、首先检查相应的Realm是否实现了实现了Authorizer

2、如果实现了Authorizer那么接着调用其相应的isPermitted*/hasRole*接口进行匹配

3、如果有一个Realm匹配那么将返回true,否则返回false

 

如果Realm进行授权的话,应该继承AuthorizingRealm,其流程是:

1.1、如果调用hasRole*,则直接获取AuthorizationInfo.getRoles()与传入的角色比较即可

1.2、如果调用如isPermitted(user:view),首先通过PermissionResolver将权限字符串转换成相应的Permission实例,默认使用WildcardPermissionResolver,即转换为通配符的WildcardPermission

2通过AuthorizationInfo.getObjectPermissions()得到Permission实例集合;通过AuthorizationInfo. getStringPermissions()得到字符串集合并通过PermissionResolver解析为Permission实例;然后获取用户的角色并通过RolePermissionResolver解析角色对应的权限集合(默认没有实现,可以自己提供);

3接着调用Permission. implies(Permission p)逐个与传入的权限比较,如果有匹配的则返回true,否则false

现在开始上代码:

Pom.xml

  1. <!-- 支持JSP,必须导入这两个依赖 -->
  2. <dependency>
  3. <groupId>org.apache.tomcat.embed</groupId>
  4. <artifactId>tomcat-embed-jasper</artifactId>
  5. <scope>provided</scope>
  6. </dependency>
  7. <dependency>
  8. <groupId>javax.servlet.jsp.jstl</groupId>
  9. <artifactId>jstl-api</artifactId>
  10. <version>1.2</version>
  11. </dependency>
  12. <dependency>
  13. <groupId>org.springframework.boot</groupId>
  14. <artifactId>spring-boot-starter</artifactId>
  15. </dependency>
  16. <dependency>
  17. <groupId>org.springframework.boot</groupId>
  18. <artifactId>spring-boot-starter-test</artifactId>
  19. <scope>test</scope>
  20. </dependency>
  21. <dependency>
  22. <groupId>org.springframework.boot</groupId>
  23. <artifactId>spring-boot-starter-web</artifactId>
  24. </dependency>
  25. <dependency>
  26. <groupId>org.apache.shiro</groupId>
  27. <artifactId>shiro-spring</artifactId>
  28. <version>1.4.0</version>
  29. </dependency>
  30. <dependency>
  31. <groupId>postgresql</groupId>
  32. <artifactId>postgresql</artifactId>
  33. <version>8.4-702.jdbc4</version>
  34. </dependency>
  35. <dependency>
  36. <groupId>org.postgresql</groupId>
  37. <artifactId>postgresql</artifactId>
  38. <scope>runtime</scope>
  39. </dependency>
  40. <dependency>
  41. <groupId>org.springframework.boot</groupId>
  42. <artifactId>spring-boot-devtools</artifactId>
  43. <optional>true</optional>
  44. </dependency>
  45. <dependency>
  46. <groupId>org.mybatis.spring.boot</groupId>
  47. <artifactId>mybatis-spring-boot-starter</artifactId>
  48. <version>1.3.0</version>
  49. </dependency>
  50. <dependency>
  51. <groupId>com.alibaba</groupId>
  52. <artifactId>druid</artifactId>
  53. <version>1.0.20</version>
  54. </dependency>
  55. <dependency>
  56. <groupId>org.apache.commons</groupId>
  57. <artifactId>commons-lang3</artifactId>
  58. <version>3.4</version>
  59. </dependency>

这边还用了Mybatis的内容,需要读者自行去学习相关的知识,这里不详细介绍了。

项目的整体预览:

SpringBoot+Shiro实现登陆拦截功能

login.jsp:这边是一个简单的form表单

  1. <form action="/loginUser" method="post">
  2. <input type="text" name="username"> <br>
  3. <input type="password" name="password"> <br>
  4. <input type="submit" value="提交">
  5. </form>

index.jsp:简单的展示界面

<h1> 欢迎登录, ${user.username} </h1>

Unauthorized.jsp:自定义跳转的无权限界面

  1. <body>
  2. Unauthorized!
  3. </body>

appliaction.yml:

  1. server:
  2. port: 8081
  3. session-timeout: 30
  4. tomcat.max-threads: 0
  5. tomcat.uri-encoding: UTF-8
  6. spring:
  7. datasource:
  8. type: com.alibaba.druid.pool.DruidDataSource
  9. driver-class-name: org.postgresql.Driver
  10. url: jdbc:postgresql://服务器地址:5432/库名
  11. username: XXXXX
  12. password: XXXXX
  13. mvc:
  14. view:
  15. prefix: /pages/
  16. suffix: .jsp
  17. mybatis:
  18. mapper-locations: mappers/*.xml
  19. type-aliases-pacakage: com.Pojo #映射的类型在Pojo下面

SpringBoot+Shiro实现登陆拦截功能这是存放的相对位置

TestController:控制器类

  1. @Controller
  2. public class TestController {
  3. @RequestMapping("/login")
  4. public String login() {
  5. return "login";
  6. }
  7. @RequestMapping("/index")
  8. public String index() {
  9. return "index";
  10. }
  11. @RequestMapping("/logout")
  12. public String logout() {
  13. Subject subject = SecurityUtils.getSubject();//取出当前验证主体
  14. if (subject != null) {
  15. subject.logout();//不为空,执行一次logout的操作,将session全部清空
  16. }
  17. return "login";
  18. }
  19. @RequestMapping("unauthorized")
  20. public String unauthorized() {
  21. return "unauthorized";
  22. }
  23. @RequestMapping("/admin")
  24. @ResponseBody//注解之后只是返回json数据,不返回界面
  25. public String admin() {
  26. return "admin success";
  27. }
  28. @RequestMapping("/edit")
  29. @ResponseBody
  30. public String edit() {
  31. return "edit success";
  32. }
  33. /*
  34. * 整个form表单的验证流程:
  35. *
  36. * 将登陆的用户/密码传入UsernamePasswordToken,当调用subject.login(token)开始,调用Relam的doGetAuthenticationInfo方法,开始密码验证
  37. * 此时这个时候执行我们自己编写的CredentialMatcher(密码匹配器),执行doCredentialsMatch方法,具体的密码比较实现在这实现
  38. *
  39. * */
  40. @RequestMapping("/loginUser")
  41. public String loginUser(@RequestParam("username") String username,
  42. @RequestParam("password") String password,
  43. HttpSession session) {
  44. UsernamePasswordToken token = new UsernamePasswordToken(username, password);
  45. Subject subject = SecurityUtils.getSubject();
  46. try {
  47. System.out.println("获取到信息,开始验证!!");
  48. subject.login(token);//登陆成功的话,放到session中
  49. User user = (User) subject.getPrincipal();
  50. session.setAttribute("user", user);
  51. return "index";
  52. } catch (Exception e) {
  53. return "login";
  54. }
  55. }
  56. }

ShiroConfiguration.java:自定义了Shiro的配置器

  1. package com.Auth;
  2. import org.apache.shiro.cache.MemoryConstrainedCacheManager;
  3. import org.apache.shiro.mgt.SecurityManager;
  4. import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
  5. import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
  6. import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
  7. import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
  8. import org.springframework.beans.factory.annotation.Qualifier;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.context.annotation.Configuration;
  11. import java.util.LinkedHashMap;
  12. @Configuration
  13. public class ShiroConfiguration {
  14. //@Qualifier代表spring里面的
  15. @Bean("shiroFilter")
  16. public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) {
  17. ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
  18. bean.setSecurityManager(manager);
  19. bean.setLoginUrl("/login");//提供登录到url
  20. bean.setSuccessUrl("/index");//提供登陆成功的url
  21. bean.setUnauthorizedUrl("/unauthorized");
  22. /*
  23. * 可以看DefaultFilter,这是一个枚举类,定义了很多的拦截器authc,anon等分别有对应的拦截器
  24. * */
  25. LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
  26. filterChainDefinitionMap.put("/index", "authc");//代表着前面的url路径,用后面指定的拦截器进行拦截
  27. filterChainDefinitionMap.put("/login", "anon");
  28. filterChainDefinitionMap.put("/loginUser", "anon");
  29. filterChainDefinitionMap.put("/admin", "roles[admin]");//admin的url,要用角色是admin的才可以登录,对应的拦截器是RolesAuthorizationFilter
  30. filterChainDefinitionMap.put("/edit", "perms[edit]");//拥有edit权限的用户才有资格去访问
  31. filterChainDefinitionMap.put("/druid/**", "anon");//所有的druid请求,不需要拦截,anon对应的拦截器不会进行拦截
  32. filterChainDefinitionMap.put("/**", "user");//所有的路径都拦截,被UserFilter拦截,这里会判断用户有没有登陆
  33. bean.setFilterChainDefinitionMap(filterChainDefinitionMap);//设置一个拦截器链
  34. return bean;
  35. }
  36. /*
  37. * 注入一个securityManager
  38. * 原本以前我们是可以通过ini配置文件完成的,代码如下:
  39. * 1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
  40. Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
  41. 2、得到SecurityManager实例 并绑定给SecurityUtils
  42. SecurityManager securityManager = factory.getInstance();
  43. SecurityUtils.setSecurityManager(securityManager);
  44. * */
  45. @Bean("securityManager")
  46. public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) {
  47. //这个DefaultWebSecurityManager构造函数,会对Subject,realm等进行基本的参数注入
  48. DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
  49. manager.setRealm(authRealm);//往SecurityManager中注入Realm,代替原本的默认配置
  50. return manager;
  51. }
  52. //自定义的Realm
  53. @Bean("authRealm")
  54. public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) {
  55. AuthRealm authRealm = new AuthRealm();
  56. //这边可以选择是否将认证的缓存到内存中,现在有了这句代码就将认证信息缓存的内存中了
  57. authRealm.setCacheManager(new MemoryConstrainedCacheManager());
  58. //最简单的情况就是明文直接匹配,然后就是加密匹配,这里的匹配工作则就是交给CredentialsMatcher来完成
  59. authRealm.setCredentialsMatcher(matcher);
  60. return authRealm;
  61. }
  62. /*
  63. * Realm在验证用户身份的时候,要进行密码匹配
  64. * 最简单的情况就是明文直接匹配,然后就是加密匹配,这里的匹配工作则就是交给CredentialsMatcher来完成
  65. * 支持任意数量的方案,包括纯文本比较、散列比较和其他方法。除非该方法重写,否则默认值为
  66. * */
  67. @Bean("credentialMatcher")
  68. public CredentialMatcher credentialMatcher() {
  69. return new CredentialMatcher();
  70. }
  71. /*
  72. * 以下AuthorizationAttributeSourceAdvisor,DefaultAdvisorAutoProxyCreator两个类是为了支持shiro注解
  73. * */
  74. @Bean
  75. public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
  76. AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
  77. advisor.setSecurityManager(securityManager);
  78. return advisor;
  79. }
  80. @Bean
  81. public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
  82. DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
  83. creator.setProxyTargetClass(true);
  84. return creator;
  85. }
  86. }

这里自定义了AuthRealm,CredentialsMatcher,来看看它们具体的代码:

  1. public class AuthRealm extends AuthorizingRealm{ //AuthenticatingRealm是抽象类,用于认证
  2. @Autowired
  3. private UserService userService;
  4. /*
  5. * 真实授权抽象方法,供子类调用
  6. *
  7. * 这个是当登陆成功之后会被调用,看当前的登陆角色是有有权限来进行操作
  8. * */
  9. @Override
  10. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  11. System.out.println("doGetAuthorizationInfo方法");
  12. User user = (User) principals.fromRealm(this.getClass().getName()).iterator().next();
  13. List<String> permissionList = new ArrayList<>();
  14. List<String> roleNameList = new ArrayList<>();
  15. Set<Role> roleSet = user.getRoles();//拿到角色
  16. if (CollectionUtils.isNotEmpty(roleSet)) {
  17. for(Role role : roleSet) {
  18. roleNameList.add(role.getRname());//拿到角色
  19. Set<Permission> permissionSet = role.getPermissions();
  20. if (CollectionUtils.isNotEmpty(permissionSet)) {
  21. for (Permission permission : permissionSet) {
  22. permissionList.add(permission.getName());
  23. }
  24. }
  25. }
  26. }
  27. SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
  28. info.addStringPermissions(permissionList);//拿到权限
  29. info.addRoles(roleNameList);//拿到角色
  30. return info;
  31. }
  32. /*
  33. * 用于认证登录,认证接口实现方法,该方法的回调一般是通过subject.login(token)方法来实现的
  34. * AuthenticationToken 用于收集用户提交的身份(如用户名)及凭据(如密码):
  35. * AuthenticationInfo是包含了用户根据username返回的数据信息,用于在匹马比较的时候进行相互比较
  36. *
  37. * shiro的核心是java servlet规范中的filter,通过配置拦截器,使用拦截器链来拦截请求,如果允许访问,则通过。
  38. * 通常情况下,系统的登录、退出会配置拦截器。登录的时候,调用subject.login(token),token是用户验证信息,
  39. * 这个时候会在Realm中doGetAuthenticationInfo方法中进行认证。这个时候会把用户提交的验证信息与数据库中存储的认证信息,将所有的数据拿到,在匹配器中进行比较
  40. * 这边是我们自己实现的CredentialMatcher类的doCredentialsMatch方法,返回true则一致,false则登陆失败
  41. * 退出的时候,调用subject.logout(),会清除回话信息
  42. *
  43. * */
  44. @Override
  45. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  46. System.out.println("将用户,密码填充完UsernamePasswordToken之后,进行subject.login(token)之后");
  47. UsernamePasswordToken userpasswordToken = (UsernamePasswordToken) token;//这边是界面的登陆数据,将数据封装成token
  48. String username = userpasswordToken.getUsername();
  49. User user = userService.findByUsername(username);
  50. return new SimpleAuthenticationInfo(user,user.getPassword(),this.getClass().getName());
  51. }
  52. }
  1. /*
  2. * 密码校验方法继承SimpleCredentialsMatcher或HashedCredentialsMatcher类,自定义实现doCredentialsMatch方法
  3. * */
  4. public class CredentialMatcher extends SimpleCredentialsMatcher {
  5. /*
  6. * 这里是进行密码匹配的方法,自己定义
  7. * 通过用户的唯一标识得到 AuthenticationInfo 然后和 AuthenticationToken (用户名 密码),进行比较
  8. * */
  9. @Override
  10. public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
  11. System.out.println("这边是密码校对");
  12. UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
  13. String password = new String(usernamePasswordToken.getPassword());
  14. String dbPassword = (String) info.getCredentials();//数据库里的密码
  15. return this.equals(password, dbPassword);
  16. }
  17. }

UserMapper.java:

  1. public interface UserMapper {
  2. User findByUsername(@Param("username") String username);
  3. }

UserMapper对应的UserMapper.xml如下:

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <mapper namespace="com.Mapper.UserMapper">
  4. <resultMap id="userMap" type="com.Pojo.User">
  5. <id property="uid" column="uid" />
  6. <id property="username" column="username" />
  7. <id property="password" column="password" />
  8. <collection property = "roles" ofType="com.Pojo.Role">
  9. <id property="rid" column="rid" />
  10. <id property="rname" column="rname" />
  11. <collection property="permissions" ofType="com.Pojo.Permission">
  12. <id property="pid" column="pid" />
  13. <id property="name" column="name" />
  14. <id property="url" column="url" />
  15. </collection>
  16. </collection>
  17. </resultMap>
  18. <select id="findByUsername" parameterType="string" resultMap="userMap">
  19. SELECT u.*, r.*, p.*
  20. FROM "user" u
  21. INNER JOIN user_role ur on ur.uid = u.uid
  22. INNER JOIN role r on r.rid = ur.rid
  23. INNER JOIN permission_role pr on pr.rid = r.rid
  24. INNER JOIN permission p on pr.pid = p.pid
  25. WHERE u.username = #{username}
  26. </select>
  27. </mapper>

这边注意,在我springBoot的启动类中,已经把包扫描了@MapperScan("com.Mapper")

@SpringBootApplication
@MapperScan("com.Mapper")
public class SpringBootShiroApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootShiroApplication.class, args);
    }
}

这边还有service类,和service的实现类:

public interface UserService {
   User findByUsername(String username);
}

@Service
public class UserServiceImpl implements UserService{
    @Resource
    private UserMapper userMapper;
    @Override
    public User findByUsername(String username) {
        return userMapper.findByUsername(username);
    }

}

另外这边也定义了几个Pojo:

User.java:用户类

  1. public class User {
  2. private Integer uid;
  3. private String username;
  4. private String password;
  5. private Set<Role> roles = new HashSet<Role>();
  6. public Integer getUid() {
  7. return uid;
  8. }
  9. public void setUid(Integer uid) {
  10. this.uid = uid;
  11. }
  12. public String getUsername() {
  13. return username;
  14. }
  15. public void setUsername(String username) {
  16. this.username = username;
  17. }
  18. public String getPassword() {
  19. return password;
  20. }
  21. public void setPassword(String password) {
  22. this.password = password;
  23. }
  24. public Set<Role> getRoles() {
  25. return roles;
  26. }
  27. public void setRoles(Set<Role> roles) {
  28. this.roles = roles;
  29. }
  30. }

Permission.java:权限类

  1. public class Permission {
  2. private Integer pid;
  3. private String name;
  4. private String url;
  5. public Integer getPid() {
  6. return pid;
  7. }
  8. public void setPid(Integer pid) {
  9. this.pid = pid;
  10. }
  11. public String getName() {
  12. return name;
  13. }
  14. public void setName(String name) {
  15. this.name = name;
  16. }
  17. public String getUrl() {
  18. return url;
  19. }
  20. public void setUrl(String url) {
  21. this.url = url;
  22. }
  23. }

Role.java:角色类

  1. public class Role {
  2. private Integer rid;
  3. private String rname;
  4. private Set<Permission> permissions = new HashSet<>();//一个角色有多个权限
  5. private Set<User> users = new HashSet<>();
  6. public Integer getRid() {
  7. return rid;
  8. }
  9. public void setRid(Integer rid) {
  10. this.rid = rid;
  11. }
  12. public String getRname() {
  13. return rname;
  14. }
  15. public void setRname(String rname) {
  16. this.rname = rname;
  17. }
  18. public Set<Permission> getPermissions() {
  19. return permissions;
  20. }
  21. public void setPermissions(Set<Permission> permissions) {
  22. this.permissions = permissions;
  23. }
  24. public Set<User> getUsers() {
  25. return users;
  26. }
  27. public void setUsers(Set<User> users) {
  28. this.users = users;
  29. }
  30. }

具体的sql如下:

  1. -------权限表------
  2. CREATE TABLE permission
  3. (
  4. pid serial NOT NULL,
  5. name character varying(255) NOT NULL,
  6. url character varying(255),
  7. CONSTRAINT permission_pkey PRIMARY KEY (pid)
  8. )
  9. WITH (
  10. OIDS=FALSE
  11. );
  12. ALTER TABLE permission
  13. OWNER TO logistics;
  14. INSERT INTO permission values('1','add','')
  15. INSERT INTO permission values('2','delete','')
  16. INSERT INTO permission values('3','edit','')
  17. INSERT INTO permission values('4','query','')
  18. -------用户表------
  19. CREATE TABLE "user"
  20. (
  21. uid serial NOT NULL,
  22. username character varying(255) NOT NULL,
  23. password character varying(255),
  24. CONSTRAINT user_pkey PRIMARY KEY (uid)
  25. )
  26. WITH (
  27. OIDS=FALSE
  28. );
  29. ALTER TABLE "user"
  30. OWNER TO logistics;
  31. INSERT INTO "user" values('1','admin','123456')
  32. INSERT INTO "user" values('2','demo','123456')
  33. -------角色表------
  34. CREATE TABLE role
  35. (
  36. rid serial NOT NULL,
  37. rname character varying(255) NOT NULL,
  38. CONSTRAINT role_pkey PRIMARY KEY (rid)
  39. )
  40. WITH (
  41. OIDS=FALSE
  42. );
  43. ALTER TABLE role
  44. OWNER TO logistics;
  45. INSERT INTO role values('1','admin')
  46. INSERT INTO role values('2','customer')
  47. -----权限角色关系表-----
  48. CREATE TABLE permission_role
  49. (
  50. rid integer NOT NULL,
  51. pid integer NOT NULL,
  52. CONSTRAINT permission_role_pkey PRIMARY KEY (pid,rid)
  53. )
  54. WITH (
  55. OIDS=FALSE
  56. );
  57. ALTER TABLE permission_role
  58. OWNER TO logistics;
  59. INSERT INTO permission_role values(1,1)
  60. INSERT INTO permission_role values(1,2)
  61. INSERT INTO permission_role values(1,3)
  62. INSERT INTO permission_role values(1,4)
  63. INSERT INTO permission_role values(2,1)
  64. INSERT INTO permission_role values(2,4)
  65. -----用户角色关系表-----
  66. CREATE TABLE user_role
  67. (
  68. rid integer NOT NULL,
  69. uid integer NOT NULL,
  70. CONSTRAINT user_role_pkey PRIMARY KEY (uid, rid)
  71. )
  72. WITH (
  73. OIDS=FALSE
  74. );
  75. ALTER TABLE user_role
  76. OWNER TO logistics;
  77. INSERT INTO user_role values(1,1)
  78. INSERT INTO user_role values(2,2)

此时我们开始测试:

输入localhost:8081/admin,由于我们在ShiroConfiguration中配置了一个拦截器链,对应的URL路径都会被对应的拦截器给拦截来处理。

  1. LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
  2. filterChainDefinitionMap.put("/index", "authc");//代表着前面的url路径,用后面指定的拦截器进行拦截
  3. filterChainDefinitionMap.put("/login", "anon");
  4. filterChainDefinitionMap.put("/loginUser", "anon");
  5. filterChainDefinitionMap.put("/admin", "roles[admin]");//admin的url,要用角色是admin的才可以登录,对应的拦截器是RolesAuthorizationFilter
  6. filterChainDefinitionMap.put("/edit", "perms[edit]");//拥有edit权限的用户才有资格去访问
  7. filterChainDefinitionMap.put("/druid/**", "anon");//所有的druid请求,不需要拦截,anon对应的拦截器不会进行拦截
  8. filterChainDefinitionMap.put("/**", "user");//所有的路径都拦截,被UserFilter拦截,这里会判断用户有没有登陆
  9. bean.setFilterChainDefinitionMap(filterChainDefinitionMap);//设置一个拦截器链

这里我们可以看到,admin路径是被roles对应的拦截器RolesAuthorizationFilter拦截,在方法isAccessAllowed中进行处理,判断是不是admin角色的用户,是这个角色的才可以访问,否则前往自己定义的无权限界面,这里别名对应的拦截器是在DefaultFilter这个枚举类中有定义:

  1. anon(AnonymousFilter.class),
  2. authc(FormAuthenticationFilter.class),
  3. authcBasic(BasicHttpAuthenticationFilter.class),
  4. logout(LogoutFilter.class),
  5. noSessionCreation(NoSessionCreationFilter.class),
  6. perms(PermissionsAuthorizationFilter.class),
  7. port(PortFilter.class),
  8. rest(HttpMethodPermissionFilter.class),
  9. roles(RolesAuthorizationFilter.class),
  10. ssl(SslFilter.class),
  11. user(UserFilter.class);

由于现在没有登录,所以一开始会前往登录界面,填写用户账号和密码,点击提交,因为我们form表单的action是loginUser,此时数据提交到Controller中对应的处理方法中:

  1. /*
  2. * 整个form表单的验证流程:
  3. *
  4. * 将登陆的用户/密码传入UsernamePasswordToken,当调用subject.login(token)开始,调用Relam的doGetAuthenticationInfo方法,开始密码验证
  5. * 此时这个时候执行我们自己编写的CredentialMatcher(密码匹配器),执行doCredentialsMatch方法,具体的密码比较实现在这实现
  6. *
  7. * */
  8. @RequestMapping("/loginUser")
  9. public String loginUser(@RequestParam("username") String username,
  10. @RequestParam("password") String password,
  11. HttpSession session) {
  12. UsernamePasswordToken token = new UsernamePasswordToken(username, password);
  13. Subject subject = SecurityUtils.getSubject();
  14. try {
  15. System.out.println("获取到信息,开始验证!!");
  16. subject.login(token);//登陆成功的话,放到session中
  17. User user = (User) subject.getPrincipal();
  18. session.setAttribute("user", user);
  19. return "index";
  20. } catch (Exception e) {
  21. return "login";
  22. }
  23. }

我们会把用户名,密码存入到UsernamePasswordToken中,UsernamePasswordToken是一个用户,密码认证令牌,里面有用户名,密码,是否缓存等属性。然后代码就会跳转到我们自己编写的Realm--AuthRealm的doGetAuthenticationInfo方法(具体可以看这篇博文https://www.cnblogs.com/ccfdod/p/6436353.html 这理由详细的介绍,这个代码调用如下:subject.login(token)-->DelegatingSubject类的login方法-->SecurityManager的login-->DefaultSecurityManager的login方法-->AuthenticatingSecurityManager的authenticate方法-->实现类AuthenticatingRealm中的getAuthenticationInfo方法)。在我们自己的getAuthenticationInfo方法中,我们根据用户名查询出用户的信息,返回AuthenticationInfo对象,如果token与获取到的AuthenticationInfo都不为空,缓存AuthenticationInfo信息。接着代码会跳转到我们的凭证验证的方法CredentialMatcher类的doCredentialsMatch方法:

  1. @Override
  2. public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
  3. System.out.println("这边是密码校对");
  4. UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
  5. String password = new String(usernamePasswordToken.getPassword());
  6. String dbPassword = (String) info.getCredentials();//数据库里的密码
  7. return this.equals(password, dbPassword);
  8. }

其实我们在调用AuthenticatingRealm的getAuthenticationInfo方法时:

  1. public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  2. AuthenticationInfo info = getCachedAuthenticationInfo(token);
  3. if (info == null) {
  4. //otherwise not cached, perform the lookup:
  5. info = doGetAuthenticationInfo(token);
  6. log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
  7. if (token != null && info != null) {
  8. cacheAuthenticationInfoIfPossible(token, info);
  9. }
  10. } else {
  11. log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
  12. }
  13. if (info != null) {
  14. assertCredentialsMatch(token, info);
  15. } else {
  16. log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
  17. }
  18. return info;
  19. }

当AuthenticationInfo查出来不为空时,进行凭证密码匹配,调用assertCredentialsMatch(token,info):

  1. protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
  2. CredentialsMatcher cm = getCredentialsMatcher();
  3. if (cm != null) {
  4. if (!cm.doCredentialsMatch(token, info)) {
  5. //not successful - throw an exception to indicate this:
  6. String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
  7. throw new IncorrectCredentialsException(msg);
  8. }
  9. } else {
  10. throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " +
  11. "credentials during authentication. If you do not wish for credentials to be examined, you " +
  12. "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
  13. }
  14. }

这边调用cm.doCredentialsMatch(token, info)方法,这边要阐述下CredentialsMatcher是一个接口,用来凭证密码匹配的,继承并实现doCredentialsMatch方法即可,这边我们自定义的CredentialMatcher类,继承了SimpleCredentialsMatcher类,而SimpleCredentialsMatcher实现了CredentialsMatcher方法。所以继续接着上面思路的进入我们的密码匹配方法,如果匹配正确则返回true,如果验证失败则返回false。此时一个完整的登录验证完成。

那么当我们继续访问其他的URL时,会进入我们授权的方法,AuthRealm类的doGetAuthorizationInfo(),主要是拿到登录用户的角色和权限,以此判断该用户是否有权限进入URL,没有权限则被跳转到unauthorized.jsp界面,( bean.setUnauthorizedUrl("/unauthorized");--原先设定的没有访问权限的情况)。

   这篇文章就讲到这,如果读者有补充,或者页面中有不对的地方请指正。