SpringBoot+Shiro实现登陆拦截功能

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

      首先介绍下Shiro:Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理,以下是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的交互用下图来表示:


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

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

这是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

<!-- 支持JSP,必须导入这两个依赖  -->
	<dependency>
           <groupId>org.apache.tomcat.embed</groupId>
           <artifactId>tomcat-embed-jasper</artifactId>
           <scope>provided</scope>
        </dependency>
        
        <dependency>  
            <groupId>javax.servlet.jsp.jstl</groupId>  
            <artifactId>jstl-api</artifactId>  
            <version>1.2</version>  
        </dependency>  
	
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter</artifactId>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
		
	<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        
        <dependency>
           <groupId>postgresql</groupId>
           <artifactId>postgresql</artifactId>
           <version>8.4-702.jdbc4</version>
        </dependency>
        
        <dependency>  
            <groupId>org.postgresql</groupId>  
            <artifactId>postgresql</artifactId>  
            <scope>runtime</scope>  
        </dependency>   
        
        <dependency>
		   <groupId>org.springframework.boot</groupId>
		   <artifactId>spring-boot-devtools</artifactId>
		   <optional>true</optional>
	</dependency>
   
        <dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.0</version>
	</dependency>
        
        <dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.0.20</version>
	</dependency>
        
        <dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.4</version>
	</dependency>

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

项目的整体预览:

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

<form action="/loginUser" method="post">
    <input type="text" name="username"> <br>
    <input type="password" name="password"> <br>
    <input type="submit" value="提交">
</form>

index.jsp:简单的展示界面

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

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

<body>
Unauthorized!
</body>

appliaction.yml:

server:  
  port: 8081
  session-timeout: 30
  tomcat.max-threads: 0
  tomcat.uri-encoding: UTF-8

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://服务器地址:5432/库名
    username: XXXXX
    password: XXXXX
  mvc:
    view:
      prefix: /pages/
      suffix: .jsp
mybatis:
  mapper-locations: mappers/*.xml
  type-aliases-pacakage: com.Pojo  #映射的类型在Pojo下面

这是存放的相对位置

TestController:控制器类

@Controller
public class TestController {

    @RequestMapping("/login")
    public String login() {
        return "login";
    }

    @RequestMapping("/index")
    public String index() {
        return "index";
    }

    @RequestMapping("/logout")
    public String logout() {
        Subject subject = SecurityUtils.getSubject();//取出当前验证主体
        if (subject != null) {
            subject.logout();//不为空,执行一次logout的操作,将session全部清空
        }
        return "login";
    }

    @RequestMapping("unauthorized")
    public String unauthorized() {
        return "unauthorized";
    }

    @RequestMapping("/admin")
    @ResponseBody//注解之后只是返回json数据,不返回界面
    public String admin() {
        return "admin success";
    }

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

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

package com.Auth;

import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;

@Configuration
public class ShiroConfiguration {
    
   //@Qualifier代表spring里面的
	
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(manager);

        bean.setLoginUrl("/login");//提供登录到url
        bean.setSuccessUrl("/index");//提供登陆成功的url
        bean.setUnauthorizedUrl("/unauthorized");
       
        /*
         * 可以看DefaultFilter,这是一个枚举类,定义了很多的拦截器authc,anon等分别有对应的拦截器
         * */
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/index", "authc");//代表着前面的url路径,用后面指定的拦截器进行拦截
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/loginUser", "anon");
        filterChainDefinitionMap.put("/admin", "roles[admin]");//admin的url,要用角色是admin的才可以登录,对应的拦截器是RolesAuthorizationFilter
        filterChainDefinitionMap.put("/edit", "perms[edit]");//拥有edit权限的用户才有资格去访问
        filterChainDefinitionMap.put("/druid/**", "anon");//所有的druid请求,不需要拦截,anon对应的拦截器不会进行拦截
        filterChainDefinitionMap.put("/**", "user");//所有的路径都拦截,被UserFilter拦截,这里会判断用户有没有登陆
        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);//设置一个拦截器链

        return bean;
    }
    
    
    /*
     * 注入一个securityManager
     * 原本以前我们是可以通过ini配置文件完成的,代码如下:
     *  1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        2、得到SecurityManager实例 并绑定给SecurityUtils
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);
     * */
    @Bean("securityManager")
    public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) {
    	//这个DefaultWebSecurityManager构造函数,会对Subject,realm等进行基本的参数注入
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(authRealm);//往SecurityManager中注入Realm,代替原本的默认配置
        return manager;
    }
    
    //自定义的Realm
    @Bean("authRealm")
    public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) {
        AuthRealm authRealm = new AuthRealm();
        //这边可以选择是否将认证的缓存到内存中,现在有了这句代码就将认证信息缓存的内存中了
        authRealm.setCacheManager(new MemoryConstrainedCacheManager());
        //最简单的情况就是明文直接匹配,然后就是加密匹配,这里的匹配工作则就是交给CredentialsMatcher来完成
        authRealm.setCredentialsMatcher(matcher);
        return authRealm;
    }
    
    /* 
     * Realm在验证用户身份的时候,要进行密码匹配
     * 最简单的情况就是明文直接匹配,然后就是加密匹配,这里的匹配工作则就是交给CredentialsMatcher来完成
     * 支持任意数量的方案,包括纯文本比较、散列比较和其他方法。除非该方法重写,否则默认值为
     * */
    @Bean("credentialMatcher")
    public CredentialMatcher credentialMatcher() {
        return new CredentialMatcher();
    }
    
    
    /*
     * 以下AuthorizationAttributeSourceAdvisor,DefaultAdvisorAutoProxyCreator两个类是为了支持shiro注解
     * */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
}

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

public class AuthRealm extends AuthorizingRealm{ //AuthenticatingRealm是抽象类,用于认证
    
	@Autowired
	private UserService userService;
	
	/*
	 * 真实授权抽象方法,供子类调用
	 * 
	 * 这个是当登陆成功之后会被调用,看当前的登陆角色是有有权限来进行操作
	 * */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		    System.out.println("doGetAuthorizationInfo方法");
		    User user = (User) principals.fromRealm(this.getClass().getName()).iterator().next();
	        List<String> permissionList = new ArrayList<>();
	        List<String> roleNameList = new ArrayList<>();
	        Set<Role> roleSet = user.getRoles();//拿到角色
	        if (CollectionUtils.isNotEmpty(roleSet)) {
	            for(Role role : roleSet) {
	                roleNameList.add(role.getRname());//拿到角色
	                Set<Permission> permissionSet = role.getPermissions();
	                if (CollectionUtils.isNotEmpty(permissionSet)) {
	                    for (Permission permission : permissionSet) {
	                        permissionList.add(permission.getName());
	                    }
	                }
	            }
	        }
	        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
	        info.addStringPermissions(permissionList);//拿到权限
	        info.addRoles(roleNameList);//拿到角色
	        return info;
	}
    
	/*
	 * 用于认证登录,认证接口实现方法,该方法的回调一般是通过subject.login(token)方法来实现的
	 * AuthenticationToken 用于收集用户提交的身份(如用户名)及凭据(如密码):
	 * AuthenticationInfo是包含了用户根据username返回的数据信息,用于在匹马比较的时候进行相互比较
	 * 
	 * shiro的核心是java servlet规范中的filter,通过配置拦截器,使用拦截器链来拦截请求,如果允许访问,则通过。
	 * 通常情况下,系统的登录、退出会配置拦截器。登录的时候,调用subject.login(token),token是用户验证信息,
	 * 这个时候会在Realm中doGetAuthenticationInfo方法中进行认证。这个时候会把用户提交的验证信息与数据库中存储的认证信息,将所有的数据拿到,在匹配器中进行比较
	 * 这边是我们自己实现的CredentialMatcher类的doCredentialsMatch方法,返回true则一致,false则登陆失败
	 * 退出的时候,调用subject.logout(),会清除回话信息
	 * 
	 * */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		System.out.println("将用户,密码填充完UsernamePasswordToken之后,进行subject.login(token)之后");
		UsernamePasswordToken  userpasswordToken = (UsernamePasswordToken) token;//这边是界面的登陆数据,将数据封装成token
		String username = userpasswordToken.getUsername();
		User user = userService.findByUsername(username);
		return new SimpleAuthenticationInfo(user,user.getPassword(),this.getClass().getName());
	}

}
/*
 * 密码校验方法继承SimpleCredentialsMatcher或HashedCredentialsMatcher类,自定义实现doCredentialsMatch方法
 * */
public class CredentialMatcher extends SimpleCredentialsMatcher {
    
	/*
	 * 这里是进行密码匹配的方法,自己定义
	 * 通过用户的唯一标识得到 AuthenticationInfo 然后和 AuthenticationToken (用户名 密码),进行比较
	 * */
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    	System.out.println("这边是密码校对");
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String password = new String(usernamePasswordToken.getPassword());
        String dbPassword = (String) info.getCredentials();//数据库里的密码
        return this.equals(password, dbPassword);
    }
}

UserMapper.java:

public interface UserMapper {
    User findByUsername(@Param("username") String username);
}

UserMapper对应的UserMapper.xml如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 

<mapper namespace="com.Mapper.UserMapper">
    <resultMap id="userMap" type="com.Pojo.User">
        <id property="uid" column="uid" />
        <id property="username" column="username" />
        <id property="password" column="password" />
        <collection property = "roles" ofType="com.Pojo.Role">
           <id property="rid" column="rid" />
           <id property="rname" column="rname" />
           <collection property="permissions" ofType="com.Pojo.Permission">
               <id property="pid" column="pid" />
               <id property="name" column="name" />
               <id property="url" column="url" />
           </collection>
        </collection>
    </resultMap>
    
    <select id="findByUsername" parameterType="string" resultMap="userMap">
      SELECT u.*, r.*, p.*
        FROM "user" u
        INNER JOIN user_role ur on ur.uid = u.uid
        INNER JOIN role r on r.rid = ur.rid
        INNER JOIN permission_role pr on pr.rid = r.rid
        INNER JOIN permission p on pr.pid = p.pid
        WHERE u.username = #{username}
    </select>
</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:用户类

public class User {
    private Integer uid;
    private String username;
    private String password;
    private Set<Role> roles = new HashSet<Role>();
    public Integer getUid() {
		return uid;
	}
	public void setUid(Integer uid) {
		this.uid = uid;
	}
	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public Set<Role> getRoles() {
		return roles;
	}
	public void setRoles(Set<Role> roles) {
		this.roles = roles;
	}	
}

Permission.java:权限类

public class Permission {
   private Integer pid;
   private String name;
   private String url;
   
    public Integer getPid() {
	    return pid;
	}
	public void setPid(Integer pid) {
		this.pid = pid;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getUrl() {
		return url;
	}
	public void setUrl(String url) {
		this.url = url;
	}
	
}

Role.java:角色类

public class Role {

    private Integer rid;

    private String rname;

    private Set<Permission> permissions = new HashSet<>();//一个角色有多个权限

    private Set<User> users = new HashSet<>();

    public Integer getRid() {
        return rid;
    }

    public void setRid(Integer rid) {
        this.rid = rid;
    }

    public String getRname() {
        return rname;
    }

    public void setRname(String rname) {
        this.rname = rname;
    }

    public Set<Permission> getPermissions() {
        return permissions;
    }

    public void setPermissions(Set<Permission> permissions) {
        this.permissions = permissions;
    }

    public Set<User> getUsers() {
        return users;
    }

    public void setUsers(Set<User> users) {
        this.users = users;
    }
}

具体的sql如下:

-------权限表------
CREATE TABLE permission
(
  pid serial NOT NULL,
  name character varying(255)  NOT NULL,
  url character varying(255),
  CONSTRAINT permission_pkey PRIMARY KEY (pid)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE permission
  OWNER TO logistics;

INSERT INTO permission values('1','add','')
INSERT INTO permission values('2','delete','')
INSERT INTO permission values('3','edit','')
INSERT INTO permission values('4','query','')

-------用户表------
CREATE TABLE "user"
(
  uid serial NOT NULL,
  username character varying(255)  NOT NULL,
  password character varying(255),
  CONSTRAINT user_pkey PRIMARY KEY (uid)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE "user"
  OWNER TO logistics;

INSERT INTO "user" values('1','admin','123456')
INSERT INTO "user" values('2','demo','123456')

-------角色表------
CREATE TABLE role
(
  rid serial NOT NULL,
  rname character varying(255)  NOT NULL,
  CONSTRAINT role_pkey PRIMARY KEY (rid)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE role
  OWNER TO logistics;

INSERT INTO role values('1','admin')
INSERT INTO role values('2','customer')

-----权限角色关系表-----
CREATE TABLE permission_role
(
  rid integer NOT NULL,
  pid integer NOT NULL,
  CONSTRAINT permission_role_pkey PRIMARY KEY (pid,rid)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE permission_role
  OWNER TO logistics;


INSERT INTO permission_role values(1,1)
INSERT INTO permission_role values(1,2)
INSERT INTO permission_role values(1,3)
INSERT INTO permission_role values(1,4)
INSERT INTO permission_role values(2,1)
INSERT INTO permission_role values(2,4)

-----用户角色关系表-----
CREATE TABLE user_role
(
  rid integer NOT NULL,
  uid integer NOT NULL,
  CONSTRAINT user_role_pkey PRIMARY KEY (uid, rid)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE user_role
  OWNER TO logistics;


INSERT INTO user_role values(1,1)
INSERT INTO user_role values(2,2)

此时我们开始测试:

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

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

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

    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);

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

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

我们会把用户名,密码存入到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方法:

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

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

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
  
        AuthenticationInfo info = getCachedAuthenticationInfo(token);  
        if (info == null) {  
            //otherwise not cached, perform the lookup:  
            info = doGetAuthenticationInfo(token);  
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);  
            if (token != null && info != null) {  
                cacheAuthenticationInfoIfPossible(token, info);  
            }  
        } else {  
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);  
        }  
  
        if (info != null) {  
            assertCredentialsMatch(token, info);  
        } else {  
            log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);  
        }  
  
        return info;  
    }  

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

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

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

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

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



  • 9
    点赞
  • 89
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值