所谓身份认证就是在应用中谁能证明他就是他本人。一般提供如他们的身份 ID 一些标识信息来表明他就是他本人,如提供身份证,用户名 / 密码来证明。
在 Shiro 中,用户需要提供 principals
(身份)和 credentials
(证明)给 shiro,从而应用能验证用户身份:
principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals
,但只有一个 Primary principals
,一般是用户名 / 密码 / 手机号。
credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。
在正式前,除了上面说到的principals和 credentials,还需要对几个概念进行说明:
Subject:主体,可以理解为用户。
UsernamePasswordToken:顾名思义,就是基于用户名和密码所生成的令牌,Realm也需要这个令牌来认证用户的信息。
Realm:Shiro通过Realm的doGetAuthenticationInfo()和doGetAuthorizationInfo()来实现对用户身份的认证和授权。在登录的这个场景下,我们需要将用户提交的身份信息与数据库中该用户的身份信息在Realm中进行对比,因此我们也可以将Realm看作是Shiro的安全数据源。
SecurityManager:安全管理器,这是整个shiro中最重要的一个组件,我们需要将刚刚提到的Realm注入到安全管理器中,才能实现认证与授权等工作。而且所有的会话、缓存也都是通过注入到安全管理器中去实现的。
ShiroFilterFactoryBean:顾名思义就是拦截器,我们可以在这里设置拦截器来拦截需要特定权限或角色的资源(url)。shiro也为我们提供了丰富的拦截器以及更细粒度的权限分级来帮助我们更好的进行权限的管理与配置。同时,SecurityManager也需要注入到其中。
整个认证流程大致是这样的:
前端将用户名密码通过接口传到后端,生成一个UsernamePasswordToken,然后调用当前Subject的login(token)方法,将token通过SecurityManager传递给Realm的doGetAuthenticationInfo(),在这个方法中实现认证。
1、配置shiro:
1.1、自定义Realm:
public class ShiroRealm extends AuthorizingRealm {
@Autowired
SysUserService sysUserService;
@Autowired
SysResourceService sysResourceService;
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = (String) token.getPrincipal();
SysUser sysUser = sysUserService.findUserByName(userName);
//用户不存在
if (sysUser == null) {
return null;
}
//验证密码,使用加盐md5进行加密
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(sysUser, sysUser.getPassword(), getName());
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(userName));
return authenticationInfo;
}
}
ps:本文暂时先只实现了登录的认证操作,因此暂时对授权这块不做处理。相关service层的代码包括数据库的表结构等之后我会打包上传,都是基础代码,也不是本文的重点,这里就不再做过多的说明了。
ps2:可以注意到,我这里SimpleAuthenticationInfo里传入的第一个参数是一个user对象,而在w3cSchool以及其他的一些例子中,传入的是userName,其实都是可以的,因为SimpleAuthenticationInfo只认证密码的有效性,用户名的有效性在之前就已经认证过了,如果用户不存在就直接返回null了。这里传入user对象,主要是方便在授权时使用,这个以后再说。
密码这块,这里做了一个以用户名作为“盐”做了一个加盐的加密处理。由于这里使用了加盐散列算法,因此我们需要对Realm(ShiroConfig.java中)设置对应规则的CredentialMatchers(用于匹配用户输入的token的凭证(未加密)与系统提供的凭证(已加密))。
1.2、shiro配置类
与SpringSecurity类似,如果想要让认证与授权流程生效,就需要在项目启动时,将Realm、SecurityManager、ShiroFilterFactoryBean加载到项目中,所以我们需要新建一个配置类ShiroConfig:
/**
* shiro配置
*/
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置过滤拦截器
Map filterMap = new LinkedHashMap<>();
/*
anon: 无需认证就能访问
authc: 必须认证才能访问
user: 必须拥有 记住我 功能才能用
perms: 拥有对某个资源的权限才能访问;
role:拥有某个角色权限才能访问;
*/
filterMap.put("/page/table", "authc");
filterMap.put("/page/form", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
//设置登录页
shiroFilterFactoryBean.setLoginUrl("/jumpToLogin");
//设置未授权页面
shiroFilterFactoryBean.setUnauthorizedUrl("/unAuthorized");
// shiroFilterFactoryBean.setSuccessUrl("/");
return shiroFilterFactoryBean;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultSecurityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm);
return securityManager;
}
@Bean(name = "shiroRealm")
public ShiroRealm shiroRealm(){
ShiroRealm shiroRealm = new ShiroRealm();
//设置CredentialMatchers
shiroRealm.setCredentialsMatcher(credentialsMatcher());
return shiroRealm;
}
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//加密方式
credentialsMatcher.setHashAlgorithmName("md5");
//加密次数
credentialsMatcher.setHashIterations(2);
// true 密码加密用hex编码; false
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
}
2、接口获取用户名,密码生成token并提交认证:
@Controller
@RequestMapping("/api")
public class ApiController {
@Autowired
SysMenuService sysMenuService;
@RequestMapping("/login")
@ResponseBody
public ResponseVO login(String username, String password) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//获取当前的Subject
Subject currentUser = SecurityUtils.getSubject();
try {
// 在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查
// 每个Realm都能在必要时对提交的AuthenticationTokens作出反应
// 所以这一步在调用login(token)方法时,它会走到xxRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法
currentUser.login(token);
//认证通过,将菜单信息返回给前端展示
if (currentUser.isAuthenticated()) {
MenuSystem menuSystem = sysMenuService.findMenuTree();
Gson gson = new Gson();
String menuJson = gson.toJson(menuSystem, MenuSystem.class);
return ResultUtil.success("登录成功!", menuJson);
}
} catch (UnknownAccountException e1) {
e1.printStackTrace();
return ResultUtil.error("用户名不存在!");
} catch (IncorrectCredentialsException e2) {
e2.printStackTrace();
return ResultUtil.error("密码输入错误!");
} catch (Exception e) {
e.printStackTrace();
return ResultUtil.error(e.getMessage());
}
return ResultUtil.error("登录失败!");
}
如果认证通过,那么通过subject.isAuthenticated()返回的结果就会是true,我们就可以认为登录成功了,此时,可以就可以将对应的菜单信息传回给前端渲染展示了。