SpringBoot+Shiro实现登陆拦截功能
上一章讲到使用自定义的方式来实现用户登录的功能,这章采用shiro来实现用户登陆拦截的功能。
首先介绍下Shiro:Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理,以下是shiro的整体的框架:
Subject: 即"用户",外部应用都是和Subject进行交互的,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过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;
2、Authorizer是真正的授权者,如果我们调用如isPermitted(“user:view”),其首先会通过PermissionResolver把字符串转换成相应的Permission实例;
3、在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限;
4、Authorizer会判断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");--原先设定的没有访问权限的情况)。
这篇文章就讲到这,如果读者有补充,或者页面中有不对的地方请指正。