shiro

1、基础操作

1.3、自定义realm

a、自定义realm

如果按照如下配置,shiro默认的 iniRealm 就会丢弃。如果想继续使用iniRealm 可以如下配置

参考博客:https://adrain-work-163-com.iteye.com/blog/2163586

securityManager.realms=$myRealm1,$iniRealm

在 shiro.ini 中配置

[main]
myRealm=com.bigguy.shiro.realm.DatabaseRealm
securityManager.realms=$myRealm

MyRealm.java

  1. extends AuthrozingRealm
  2. 一般重写 getName方法
  3. 重写两个方法
    1. doGetAuthorizationInfo:表示授权
    2. 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)]

  1. 假设,表单提交的action地址和 authc.loginUrl 一致
  2. 用户在浏览器输入 username,password 点击提交
  3. 会先被 authc 拦截到,然后拿着 username,password,去shiro.ini 文件中的 [users]下查找是否有对于的username和 password
  4. 如果有表示认证成功,跳转到authc.successUrl 对于的地址(默认为 “/”)
  5. 如果没有找到,在转到 LoginServlet 进行验证
a、代码
  1. 假设用户访问:localhost:8080/main ,这个路径匹配 /**=authc,被authc拦截器拦截
  2. 跳转到 /login -> LoginServlet.doGet()方法 -> 跳转到 /login.jsp
  3. login.jsp 输入 username,password 提交到 -> /login -> authc 先拦截,去shiro.ini 文件中匹配[users]。
    1. 匹配:跳转到 authc.successUrl
    2. 未匹配:就再转到 -> /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;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值