1、基础操作
1.3、自定义realm
a、自定义realm
如果按照如下配置,shiro默认的 iniRealm 就会丢弃。如果想继续使用iniRealm 可以如下配置
securityManager.realms=$myRealm1,$iniRealm
在 shiro.ini 中配置
[main]
myRealm=com.bigguy.shiro.realm.DatabaseRealm
securityManager.realms=$myRealm
MyRealm.java
- extends AuthrozingRealm
- 一般重写 getName方法
- 重写两个方法
- doGetAuthorizationInfo:表示授权
- doGetAuthenticationInfo:表示验证
public class DatabaseRealm extends AuthorizingRealm {
@Override
public String getName() {
return "dataBaseRealm";
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 能进到这里说明验证成功
String username =(String) principals.getPrimaryPrincipal();
List<String> permissions = UserDaoUtils.getPermissions(username);
List<String> roles = UserDaoUtils.getRoles(username);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 授权
simpleAuthorizationInfo.addRoles(roles);
simpleAuthorizationInfo.addStringPermissions(permissions);
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
// 此处假设 token.getPrincipal();存储的是用户的唯一标识(这个username,可以是电话、邮箱、用户名... 是用户唯一的标识)
// 此时传递过来的是表单中进验证的 username, password
// 强转为UsernamePasswordToken 可以拿到用户名、密码
UsernamePasswordToken t = (UsernamePasswordToken)token;
String username = (String)t.getPrincipal(); // 拿到用户名
String password = new String(t.getPassword()); // 拿到密码
// 模拟数据库
String realPass = UserDaoUtils.getPass(username); // 系统用户目前只有 tom/admin
if(realPass==null || !realPass.equals(password)){
throw new AuthenticationException(); // 表示认证失败
}
// 返回的表示数据库中真正的用户名和密码
// 返回后会将这个数据库用户名密码 在其他方法中与 用户登入的用户名和密码比较
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName());
return info;
}
}
b、加密操作
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal(); // 这里的 principal 一般是用户的唯一标识,此处标识username在数据库中是唯一的
User user = userDao.findUserByUsername(username);
String password = user.getPassword(); // 数据库中取出的密码(该密码一般是加密的)
// ByteSource.Util.bytes(username) 参数指定加密的盐
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes(username), getName());
return simpleAuthenticationInfo;
}
- 这里返回 SimpleAuthenticaionInfo 后,会拿着数据库的username,和 password和盐,在某处和登入的 token(里面有登入时的 username,和password)进行比较
- 比较的时候拿着登入的密码 和 SimpleAuthenticaionInfo 传递过来的盐进行加密后和 SimpleAuthenticaionInfo 里面的 password(数据库里面真实的密码)进行比较,若匹配则表示验证成功
底层代码
类:AuthenticatingRealm
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token){
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
info = doGetAuthenticationInfo(token); // 跳到自定义realm 执行验证操作,并返回数据库的密码
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
assertCredentialsMatch(token, info); // 进行 info(数据库密码)和 登入 token 验证
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
// 后跳到该类的:assertCredentialsMatch方法进行密码的匹配(加盐匹配操作)
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info){
CredentialsMatcher cm = getCredentialsMatcher();
if (cm != null) {
if (!cm.doCredentialsMatch(token, info)) { // 进行比较
...
}
} else {
...
}
}
// 跳到 HashedCredentialsMatcher
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
// 如果 info 有盐,就让 token(保存登入时密码)进行加密操作(加盐)
Object tokenHashedCredentials = hashProvidedCredentials(token, info);
Object accountCredentials = getCredentials(info);
return equals(tokenHashedCredentials, accountCredentials);
}
2、整合 servlet
2.1 authc 拦截器
authc拦截器的作用:
- 进行登入验证,拦截判断用户是否登入,如果登入直接放行。如果没登入跳转到authc.loginUrl 属性配置的路径(默认是 /login.jsp)
- 执行登录认证(表单认证):在表单中输入username,password,如果表单的提交地址和 authc.loginUrl 地址一致。会先从 用户名密码传递到realm中进行查找,是否realm有。假设是配置文件的方式。如果在 [users]下找到对应的配置,则登入成功,跳转到 authc.successUrl 路径下(默认"/"),就不会走 LoginServlet的逻辑了
[外链图片转存失败(img-dgVuPSsw-1563524719523)(C:\Users\Administrator\Desktop\技术总结\视频文档\shiro\imgs\authc.png)]
- 假设,表单提交的action地址和 authc.loginUrl 一致
- 用户在浏览器输入 username,password 点击提交
- 会先被 authc 拦截到,然后拿着 username,password,去shiro.ini 文件中的 [users]下查找是否有对于的username和 password
- 如果有表示认证成功,跳转到authc.successUrl 对于的地址(默认为 “/”)
- 如果没有找到,在转到 LoginServlet 进行验证
a、代码
- 假设用户访问:localhost:8080/main ,这个路径匹配 /**=authc,被authc拦截器拦截
- 跳转到 /login -> LoginServlet.doGet()方法 -> 跳转到 /login.jsp
- login.jsp 输入 username,password 提交到 -> /login -> authc 先拦截,去shiro.ini 文件中匹配[users]。
- 匹配:跳转到 authc.successUrl
- 未匹配:就再转到 -> /login -> LoginServlet.doGet()进行身份验证。
shiro.ini
[main]
#默认是/login.jsp
authc.loginUrl=/login
# 登入成功后跳转的地址
authc.successUrl=/WEB-INF/admin/main.jsp
#用户无需要的角色时跳转的页面
roles.unauthorizedUrl=/nopermission.jsp
#用户无需要的权限时跳转的页面
perms.unauthorizedUrl=/nopermission.jsp
#登出之后重定向的页面
logout.redirectUrl=/main
[users]
admin=666,admin
zhangsan=666,deptMgr
[roles]
admin=employee:*,department:*
deptMgr=department:view
[urls]
#静态资源可以匿名访问
/static/**=anon
/**=authc
LoginServlet
表示只有 hechen/hechen 的用户才能验证通过
protected void doGet(HttpServletRequest req, HttpServletResponse resp){
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println("login");
if("hechen".equals(username) && "hechen".equals(password)){
req.setAttribute("username", username);
req.getRequestDispatcher("/main").forward(req, resp);
}else {
req.setAttribute("error", "用户名密码错误!");
req.getRequestDispatcher("/login.jsp").forward(req, resp);
}
}
MainServlet
跳转到后台的 servlet
@WebServlet("/main")
public class MainServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp){
// 表示跳转到后台
req.getRequestDispatcher("/WEB-INF/admin/main.jsp").forward(req, resp);
}
// ...
}
b、注意
请求中账号与密码必须固定为username 跟password,
如果需要改动必须在配置文件中额外指定authc.usernameParam=xxx
authc.passwordParam=xxxx
c、源代码
authc -> FormAuthenticationFilter
// 执行验证 AuthenticatingFilter
protected boolean executeLogin(ServletRequest request, ServletResponse response){
// 源码看下面
AuthenticationToken token = createToken(request, response);
if (token == null) {
// 验证失败,报错
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
// 执行验证 -> 走 subject.login()那套源码,说明除了可以从 shiro.ini 拿到对应的用户名密码,也可以从自定义 realm 中验证(会将 username,password 传递过去)
subject.login(token);
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
// 创建 验证token
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
String username = getUsername(request); // 拿到username
String password = getPassword(request); // 拿到密码
return createToken(username, password, request, response);
}
protected String getUsername(ServletRequest request) {
return WebUtils.getCleanParam(request, getUsernameParam());
}
public String getUsernameParam() {
return usernameParam; // 系统默认是 "username",可以在shiro.ini 中自定义
}
// shiro.ini 配置
authc.usernameParam=xxx
authc.passwordParam=xxxx
2.2 修改 LoginServlet
- loginServlet 的 /login 必须被 authc 拦截器拦截才会进入
- 因为用户认证表单提交会经过 authc 过滤器先验证,如果验证通过,会在request中设置错误消息
“shiroLoginFailure”,所以只需要拿到这个消息即可并判断做相应逻辑。
- 这里的 LoginServlet 现在不处理登入验证逻辑处理(用户名密码与数据库是否匹配)
- 如果在 realm 中认证成功,会自动跳转到 authc.successUrl ,所以不用再 LoginServlet 中处理跳转成功页面逻辑。
protected void doGet(HttpServletRequest req, HttpServletResponse resp){
//如果登陆失败从request中获取认证异常信息,shiroLoginFailure就是shiro异常类的全限定名
String exceptionClassName = (String) req.getAttribute("shiroLoginFailure");
//根据shiro返回的异常类路径判断,抛出指定异常信息
if(exceptionClassName!=null){
if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
//最终会抛给异常处理器
req.setAttribute("errorMsg","账号不存在");
} else if (IncorrectCredentialsException.class.getName().equals(
exceptionClassName)) {
req.setAttribute("errorMsg","用户名/密码错误");
} else {
req.setAttribute("errorMsg","其他异常信息");//最终在异常处理器生成未知错误
}
}
//此方法不处理登陆成功(认证成功),shiro认证成功会自动跳转到上一个请求路径
//登陆失败还到login页面
req.getRequestDispatcher("/login.jsp").forward(req, resp);
}
3、整合Spring
3.1、整合Spring流程
a、添加 jar依赖
主要是添加 shiro-spring
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.2</version>
</dependency>
b、在 web.xml 配置filter
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
c、在Spring 配置文件中配置 shiro相关配置
为了使每个spring的xml文件看起来简洁,命名为 spring-shiro.xml
3.2 基于注解的权限认证
对每一个 url访问配置对应的权限有两种方法
- 在类似 ini 文件中配置:/deleteOrder.jsp=authc,perms[“deleteOrder”]
- 在访问的Controller 上加上权限注解 :灵活
a、配置
需要在 Spring 中配置,开启shiro注解
<!-- 开启aop,对类代理 -->
<aop:config proxy-target-class="true"></aop:config>
<!-- 开启shiro注解支持 -->
<bean class="org.apache.shiro.spring.security.interceptor.
AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
b、权限注解一般标记在Controller 层
@RequiresPermissions("admin:list")
@ResponseBody
@RequestMapping("/list")
public String list(){
return "list";
}
c、配置权限异常跳转页面
如果访问一个没有权限的页面,会报错,直接跳转到错误页面
[外链图片转存失败(img-pKYKJzSc-1563524719524)(imgs\权限错误页面.png)]
可以配置异常页面,当权限不够使,自动跳转到“权限不足”提示页面
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="org.apache.shiro.authz.UnauthorizedException">redirect:/nopermission.jsp</prop>
</props>
</property>
</bean>
3.3 加密操作
步骤:
- 配置加密器
- 在 realm 中注入加密器
<bean id="credentialsMatcher"
class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="md5" />
<property name="hashIterations" value="1" />
</bean>
<!-- 自定义Realm -->
<bean id="userRealm" class="cn.wolfcode.shiro.realm.UserRealm">
<property name="credentialsMatcher" ref="credentialsMatcher" />
</bean>
自定义 realm
认证操作
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken userToken = (UsernamePasswordToken)token;
// 假定 username 是user中的唯一标识
String username = (String)userToken.getPrincipal();
User user = userDao.findUserByUsername(username); // 从数据库中找出 user
// 进行数据库查询,返回密码
String password = user.getPassword(); // 数据库密码(加密的)
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes(username),getName());
return info;
}