shiro框架学习使用记录

摘要:最近项目需要使用到权限管理,与登录安全,于是开始学习Shiro这个框架,更易于解决项目问题。此篇记录自己学习该框架历程与Demo。


一、环境、依赖包及简单实现身份认证Demo

1.1 Maven引入依赖包

通过maven自动引入Shiro依赖包

1
2
3
4
5
6
7
<!--org.apache.shiro依赖包 开始-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
<!--shiro结束-->

1.2 简单身份登录验证Demo

1.2.1 简单身份凭证(shirotest1.ini)

1
2
3
[users]
用户1=42524
用户2=4514524

1.2.2 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testShiro1(){
//1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
Factory<SecurityManager> factory =
new IniSecurityManagerFactory("classpath:shiroTest/shirotest1.ini");
//2、得到SecurityManager实例 并绑定给SecurityUtils
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//3、得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("用户1", "42524");

try {
//4身份认证
subject.login(token);
}catch(AuthenticationException e){
System.out.println("身份认证失败!");
}
Assert.assertEquals(true,subject.isAuthenticated());//断言用户已经登录

//6.退出
subject.logout();
}

1.2.3 Demo结果

验证成功

Alt text

验证失败

Alt text

测试问题

如上测试的几个问题:
1、用户名/密码硬编码在ini配置文件,以后需要改成如数据库存储,且密码需要加密存储;
2、用户身份Token可能不仅仅是用户名/密码,也可能还有其他的,如登录时允许用户名/邮箱/手机号同时登录。

———————————————–摘自《跟我学Shiro》

二、身份认证流程与Realm域

2.1 身份认证流程

Alt text

引用自:http://jinnianshilongnian.iteye.com/blog/2018936
张开涛《跟我学Shiro》系列

2.1.1 流程

  • 1、首先调用Subject.login(token)进行登录,其会自动委托给Security Manager,调用之前必须通过SecurityUtils. setSecurityManager()设置;
  • 2、SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证;
  • 3、Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现;
  • 4、Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证;
  • 5、Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。

2.2 Realm 域

 Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。之前的ini配置方式将使用org.apache.shiro.realm.text.IniRealm。

以下是官方文档:

A Realm is a component that can access application-specific security data such as users, roles, and permissions. The Realm translates this application-specific data into a format that Shiro understands so Shiro can in turn provide a single easy-to-understand Subject programming API no matter how many data sources exist or how application-specific your data might be.
Realms usually have a 1-to-1 correlation with a data source such as a relational database, LDAP directory, file system, or other similar resource. As such, implementations of the Realm interface use data source-specific APIs to discover authorization data (roles, permissions, etc), such as JDBC, File IO, Hibernate or JPA, or any other Data Access API.
—————————————– http://shiro.apache.org/realm.html

2.2.1 Realm 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.apache.shiro.realm;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;

public interface Realm {
String getName(); //返回一个唯一的Realm名字

boolean supports(AuthenticationToken var1);/判断此Realm是否支持此Token

AuthenticationInfo getAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;//根据Token获取认证信息

}

2.2.2 自定义Realm实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyRealm1 implements Realm {  
@Override
public String getName() {
return "myrealm1";
}
@Override
public boolean supports(AuthenticationToken token) {
//仅支持UsernamePasswordToken类型的Token
return token instanceof UsernamePasswordToken;
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal(); //得到用户名
String password = new String((char[])token.getCredentials()); //得到密码
if(!"zhang".equals(username)) {
throw new UnknownAccountException(); //如果用户名错误
}
if(!"123".equals(password)) {
throw new IncorrectCredentialsException(); //如果密码错误
}
//如果身份认证验证成功,返回一个AuthenticationInfo实现;
return new SimpleAuthenticationInfo(username, password, getName());
}
}

2.2.3 ini配置文件自定义Realm实现

1
2
3
4
5
#声明一个realm  
myRealm1=me.hgx.MyRealm1
#指定securityManager的realms实现
securityManager.realms=$myRealm1
#通过$name来引入之前的realm定义

2.2.4 测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TestRealm {
@Test
public void testCustomRealm(){
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiroTest/shiro-custom-realm.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("zhang","123");

try {
//4、登录,即身份验证
subject.login(usernamePasswordToken);
} catch (AuthenticationException e) {
//5、身份验证失败
}

Assert.assertEquals(true, subject.isAuthenticated()); //断言用户已经登录

//6、退出
subject.logout();
}
@After
public void tearDown() throws Exception {
ThreadContext.unbindSubject();//退出时请解除绑定Subject到线程 否则对下次测试造成影响
}
}

三、授权流程及接口实现

3.1 授权流程

Alt text

引用自:http://jinnianshilongnian.iteye.com/blog/2018936
张开涛《跟我学Shiro》系列

3.1.1 授权过程中的关键概念

  • 1、授权(Authorization)也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等);
  • 2、主体(Subject)主体,即访问应用的用户,在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。
  • 3、资源(Resource)资源 在应用中用户可以访问的任何东西,比如访问JSP页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
  • 4、权限(Permission)安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:一、访问用户列表页面;二、查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权限控制;三、打印文档等等…
  • 5、角色(Role)角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。

    3.1.2 流程

    3.1.2.1 流程如下:

  • 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表示授权失败。

3.1.2.2 ModularRealmAuthorizer进行多Realm匹配流程:

  • 1、首先检查相应的Realm是否实现了实现了Authorizer;
  • 2、如果实现了Authorizer,那么接着调用其相应的isPermitted/hasRole接口进行匹配;
  • 3、如果有一个Realm匹配那么将返回true,否则返回false。

3.1.2.3 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。

3.2 Authorizer、PermissionResolver及RolePermissionResolver

 Authorizer的职责是进行授权(访问控制),是Shiro API中授权核心的入口点,其提供了相应的角色/权限判断接口,具体请参考其Javadoc。SecurityManager继承了Authorizer接口,且提供了ModularRealmAuthorizer用于多Realm时的授权匹配。PermissionResolver用于解析权限字符串到Permission实例,而RolePermissionResolver用于根据角色解析相应的权限集合。

具体实现用例见后文项目实现

四、加盐加解密

4.1 编码/解码

 Shiro提供了base64和16进制字符串编码/解码的API支持,方便一些编码解码操作。Shiro内部的一些数据的存储/表示都使用了base64和16进制字符串。

1
2
3
4
String str = "hello";  
String base64Encoded = Base64.encodeToString(str.getBytes());
String str2 = Base64.decodeToString(base64Encoded);
Assert.assertEquals(str, str2);

通过如上方式可以进行base64编码/解码操作,更多API请参考其Javadoc。

1
2
3
4
String str = "hello";  
String base64Encoded = Hex.encodeToString(str.getBytes());
String str2 = new String(Hex.decode(base64Encoded.getBytes()));
Assert.assertEquals(str, str2);

 通过如上方式可以进行16进制字符串编码/解码操作,更多API请参考其Javadoc。

 还有一个可能经常用到的类CodecSupport,提供了toBytes(str, “utf-8”) / toString(bytes, “utf-8”)用于在byte数组/String之间转换。

4.2 散列算法

 常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一些md5解密网站很容易的通过散列值得到密码“admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和ID(即盐);这样散列的对象是“密码+用户名+ID”,这样生成的散列值相对来说更难破解。

1
2
3
4

String str = "hello";
String salt = "123";
String md5 = new Md5Hash(str, salt).toString();//还可以转换为 toBase64()/toHex()

如上代码通过盐“123”MD5散列“hello”。另外散列时还可以指定散列次数,如2次表示:md5(md5(str)):“new Md5Hash(str, salt, 2).toString()”。

1
2
3
String str = "hello";  
String salt = "123";
String sha1 = new Sha256Hash(str, salt).toString();

使用SHA256算法生成相应的散列数据,另外还有如SHA1、SHA512算法。

Shiro还提供了通用的散列支持:

1
2
3
4
String str = "hello";  
String salt = "123";
//内部使用MessageDigest
String simpleHash = new SimpleHash("SHA-1", str, salt).toString();

通过调用SimpleHash时指定散列算法,其内部使用了Java的MessageDigest实现。

为了方便使用,Shiro提供了HashService,默认提供了DefaultHashService实现.

1
2
3
4
5
6
7
8
9
10
11
12

DefaultHashService hashService = new DefaultHashService(); //默认算法SHA-512
hashService.setHashAlgorithmName("SHA-512");
hashService.setPrivateSalt(new SimpleByteSource("123")); //私盐,默认无
hashService.setGeneratePublicSalt(true);//是否生成公盐,默认false
hashService.setRandomNumberGenerator(new SecureRandomNumberGenerator());//用于生成公盐。默认就这个
hashService.setHashIterations(1); //生成Hash值的迭代次数

HashRequest request = new HashRequest.Builder()
.setAlgorithmName("MD5").setSource(ByteSource.Util.bytes("hello"))
.setSalt(ByteSource.Util.bytes("123")).setIterations(2).build();
String hex = hashService.computeHash(request).toHex();

1、首先创建一个DefaultHashService,默认使用SHA-512算法;
2、可以通过hashAlgorithmName属性修改算法;
3、可以通过privateSalt设置一个私盐,其在散列时自动与用户传入的公盐混合产生一个新盐;
4、可以通过generatePublicSalt属性在用户没有传入公盐的情况下是否生成公盐;
5、可以设置randomNumberGenerator用于生成公盐;
6、可以设置hashIterations属性来修改默认加密迭代次数;
7、需要构建一个HashRequest,传入算法、数据、公盐、迭代次数。

SecureRandomNumberGenerator用于生成一个随机数:

1
2
3
4
SecureRandomNumberGenerator randomNumberGenerator =  
new SecureRandomNumberGenerator();
randomNumberGenerator.setSeed("123".getBytes());
String hex = randomNumberGenerator.nextBytes().toHex();

五、 SpringMVC 与 Shiro结合应用

5.1 SpringMVC配置

首先需要在SpringMVC的xml文件中,添加如下拦截器

1
2
3
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>

完整的SpringMVC配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">


<!--在dispatcher-servlet.xml中主要是配置springmvc的一些Controller的初始化,静态文件的映射策略,视图的配置等。-->
<context:annotation-config/>
<context:component-scan base-package="com.nist.targetsystem.controller">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>

<!--默认的注解映射的支持-->
<!--启用spring mvc 注解-->
<mvc:annotation-driven>
<!-- 启动JSON格式的配置 -->
<mvc:message-converters>
<!-- 这里也可以自己定制class -->
<bean id="jsonConverter" class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter"><!-- 这里也可以自己定制class -->
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value> <!-- 避免IE出现下载JSON文件的情况 -->
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!--=============================视图配置================================================================-->
<!-- ViewResolver -->
<!-- 视图解释类 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/" />
<!--可为空,方便实现自已的依据扩展名来选择视图解释类的逻辑 -->
<property name="suffix" value=".jsp" />
</bean>
</beans>

5.2 Spring-Shiro 配置

完整的Shiro配置如下所示(未配置缓存),不同bean的含义见bean上的注释,配置文件略去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:property-placeholder location="classpath:shiro.properties" ignore-unresolvable="true"/>
<!-- 自定义Realm -->
<bean id="UserRealm" class="com.nist.targetsystem.shiro.realm.UserRealm">
<!--<property name="credentialsMatcher" ref="passwordMatcher"/>-->
</bean>
<!--&lt;!&ndash; 自定义验证匹配器&ndash;&gt;-->
<!--<bean id="passwordMatcher" class="com.nist.targetsystem.shiro.matcher.UserCredentialsMatcher"/>-->
<!-- 会话管理器-->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="${globalSessionTimeout}"/>
<property name="deleteInvalidSessions" value="${deleteInvalidSessions}"/>
<property name="sessionValidationSchedulerEnabled" value="${sessionValidationSchedulerEnable}"/>
<property name="sessionIdCookieEnabled" value="${sessionIdCookieEnabled}"/>
</bean>

<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="UserRealm"/>
<property name="sessionManager" ref="sessionManager"/>
</bean>

<!-- Shiro过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager"/>
<!-- 身份认证失败,则跳转到登录页面的配置 -->
<property name="loginUrl" value="/Login/index"/>
<!-- 身份认证成功,跳转到首页-->
<property name="successUrl" value="/administrator/index"/>
<!-- 权限认证失败,则跳转到指定页面 -->
<property name="unauthorizedUrl" value="/Login/unauthor"/>
<!-- Shiro连接约束配置,即过滤链的定义 -->
<property name="filterChainDefinitions">
<value>
/media/**=anon
/Login/index=anon
/administrator/**=authc
/administrator/**=roles[admin]
/Login/logout =logout
/administrator/**=user
</value>
</property>
</bean>

<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

<!-- 开启Shiro注解 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>

5.3 SpringMVC 实践(单以Login为例)

5.3.1 Controller层

5.3.1.1 UserController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* User的登陆,注册和验证的控制层
* Created by huangguoxin on 16/3/29.
*/

@Controller
@RequestMapping("/Login")
public class UserController {

private static Logger logger = LoggerFactory.getLogger(UserController.class);

@Autowired
@Qualifier("userService")
private UserService userService; //Service接口

/**
* 跳转到登陆页面
* @return 跳转到AdminLogin文件夹下的login.jsp
*/

@RequestMapping(value = "/index")
public String loginIndex() {
return "AdminLogin/login";
}

/**
* 实现登陆校验,交由service.loginHandle()方法处理登陆,Controller进行跳转与异常处理
* @param request 获取传送表单的request到该Url(Login/login)
* @param model 返回的Model,使用RedirectAttribute,可使重定向后的Url不带参数
* @return 成功:跳转到administrator/index(该字符串为URl)(已由shiro配置文件处理,自动跳转)
* 失败:返回错误model信息回到Login/index,并显现
*/

@RequestMapping(value = "/login",method = RequestMethod.POST)
public String login(HttpServletRequest request, RedirectAttributes model){

String userName = request.getParameter("username").trim();
String password = request.getParameter("password");
logger.info("登陆用户名:{}",userName);

try{
String loganUserName = userService.loginHandle(userName,password);
model.addAttribute("username",loganUserName);
return "redirect:/administrator/index";
}catch (UnknownAccountException e){
logger.info("登陆失败原因:{}","UnknownAccount,无该用户");
logger.info("登陆失败用户名:{}",userName);
model.addFlashAttribute("error","用户名或密码错误!");
return "redirect:/Login/index";
}catch (IncorrectCredentialsException e){
logger.info("登陆失败原因:{}","IncorrectCredentials");
logger.info("登陆失败密码:{}",password);
model.addFlashAttribute("error","用户名或密码错误!");
return "redirect:/Login/index";
}
catch (Exception e){
logger.info("登陆失败原因:{}",e.getCause().getMessage());
logger.info("登陆失败用户名:{}",userName);
logger.info("登陆失败密码:{}",password);
model.addFlashAttribute("error","用户名或密码错误!");
return "AdminLogin/login";
}
}
}

5.3.1.2 Controller层解析

  • 1、我们配置文件配置了

    /administrator = user

 说明当用户未通过Shiro框架验证和授权,是无法访问URL前缀为/administrator的地址,Shiro拦截器自动将这次访问重定向到/Login/index登陆页面,详见shiro.xml:

/Login/index=anon

 anon 参数的含义即:所有未授权用户都具有访问权限,Controller将这个URl映射到 ..Admin/login.jsp这个JSP页面中,但URL隐藏了真正的视图名,并且参数自带在URL后,不再显示的使用传统的html形式传参数,将这种MVC模式称为RESTful客户端形式。

  • 2、 之后表单填写,并将其以post的形式请求到URL:/Login/login,在UserController这个Controller层中的login方法将参数交付于UserService接口使用,校验信息。成功这重定向到用户首页,UserService如抛出任何异常,则重定向到Login首页,并在Login首页的错误信息块中显示错误信息。
  • 3、定义UserService接口,交由Spring容器进行注入,下面是UserSerive的实现

5.3.2 UserService层

5.3.2.1 UserService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**UserService实现类
* Created by huangguoxin on 16/3/29.
*/

//是否能使用SpringAop,异常与前期输入验证相结合起来?
@Service("userService")
public class UserServiceImpl implements UserService {

private static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

@Resource
private UserDao userDao;

@Resource
private UserPasswordHelper userPasswordHelper;
/**
* 登陆验证处理服务
* @param userName
* @param password
* @return
* @throws IncorrectCredentialsException 向上抛出验证异常,交由UserController
*/

public String loginHandle(String userName,String password) {

Subject subject = SecurityUtils.getSubject();
User user = getByUserName(userName);

if("".equals(userName) && user == null){//如数据库无该用户
throw new UnknownAccountException();
}else { //无则继续登陆流程
UsernamePasswordToken token = new UsernamePasswordToken(
userName,
encryptPassword(password, userName + user.getSalt())//加密密码
);

subject.login(token);
Session session = subject.getSession();
session.setAttribute("userName", user.getUserName());//用户的账户名放入session中使用
}
return userName;
}

/**
* 实现获取指定userName的User信息
* @param userName 用户名
* @return
*/

public User getByUserName(String userName) {
return userDao.getByUserName(userName);
}

/**
* 获取用户角色
* @param userName 用户名称
* @return 角色Set集合
*/

public Set<String> getRoles(String userName) {
return userDao.getRoles(userName);
}

/**
* 获取用户权限
* @param userName 用户名称
* @return 权限Set集合
*/

public Set<String> getPermissions(String userName) {
return userDao.getPermissions(userName);
}

public String encryptPassword(String password,String salt){
return userPasswordHelper.encrypt(password,salt);
}
}

5.3.2.2 UserService层解析

  • 1、loginHandle接受Controller传来的用户名与未加密的密码,使用当前传来的UserName查询数据库,得到该用户信息,判断是否有该用户,有则通过PasswordHelper接口中的加密函数将用户的输入的密码和从数据库中取出的盐加密,一起放入用户Token中,用过实体类Subject中的函数login进行登陆。如果无用户,则Controller层抛出异常。
  • 2、UserService中需要注入两个接口,一个是UserDao的数据库操作,细节不表。第二个是PasswordHelper接口,该接口通过Spring容器注入一个PasswordHelper实现类。该接口的实现类要完成,加密密码,生成随机盐等功能。
  • 3、剩下的getPermission与getRoles函数,是为Realm域服务的功能,为了获取当前用户的角色与特权。

5.3.3 Realm域的实现

5.3.3.1 UserRealm.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
**
* 自定义实现Realm,实现接口
* Created by huangguoxin on 16/3/29.
*/
public class UserRealm extends AuthorizingRealm{

@Autowired
@Qualifier("userService")
private UserService userService;

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String userName = (String)principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(userService.getRoles(userName));
authorizationInfo.setStringPermissions(userService.getPermissions(userName));
return authorizationInfo;
}

/**
* 根据Token获取认证信息
* @param authenticationToken
* @return
* @throws AuthenticationException
*/

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String userName = (String) authenticationToken.getPrincipal();
User user = userService.getByUserName(userName);
if(user == null) {
throw new UnknownAccountException();//用户未找到
}
return new SimpleAuthenticationInfo(
user.getUserName(),
user.getPassword(),
getName()
);
}

}

5.3.3.2 Realm自定义域解析(见上面授权与验证中,此处省略)