第一步:添加依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.10.0</version>
</dependency>
第二步:创建Config类与UerRealm
在shiro中有这几个权限级别
anon: 无需认证就可以访问
authc: 必须认证了才能访问
user: 必须拥有 记住我才能访问
perms: 拥有对摸个资源的权限才能访问
role: 拥有某个角色的访问权限才能访问
从外部看,shiro通过以下三层架构来实现session,登录认证,密码加密,等待一系列功能业务
从内部看,shiro支持以下这几种功能
shiroConfig中主要有三个方法也基本分别对应图中的三层架构
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager SecurityManager)
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm)
public UserRealm userRealm()
其中UserRealm是需要我们自定义的
UerRealm需要继承AuthorizingRealm并实现里面的
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) //授权认证
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException //资格认证
就如注释上写的,doGetAuthenticationInfo主要负责资格认证,
在shiroFilterFactoryBean中我们需要通过HashMap创建一个filterMap并通过setFilterChainDefinitionMap放入创建的ShiroFilterFactoryBean类对象中(这里声明为bean)
例如以下一系列权限声明
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager SecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(SecurityManager);
//添加shiro的内置过滤器
/*
anon: 无需认证就可以访问
authc: 必须认证了才能访问
user: 必须拥有 记住我才能访问
perms: 拥有对摸个资源的权限才能访问
role: 拥有某个角色的访问权限才能访问
*/
HashMap filterMap = new LinkedHashMap();
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");
filterMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterMap);
//设置需要认证时访问的controller
bean.setLoginUrl("/toLogin");
//设置需要授权时访问的controller
bean.setUnauthorizedUrl("/noAuth");
return bean;
}
相对的在同一个包中,我们需要注入一个DefaultWebSecurityManager,配置相关的属性,例如加密方式,迭代次数,绑定Realm等操作。
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(){
// 创建defaultWebSecurityManager对象
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//创建加密对象,设置相关属性
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//采用md5加密
matcher.setHashAlgorithmName("md5");
//设置迭代次数
matcher.setHashIterations(2);
//将加密对象存储到userRealm对象当中
userRealm.setCredentialsMatcher(matcher);
//将userRealm存储到defaultWebSecurityManager当中
defaultWebSecurityManager.setRealm(userRealm);
return defaultWebSecurityManager;
};
认证时所需要的操作(也就是authc级别)
以下是登录时所需要进行的操作,将数据加密封装到token中(这里是默认的明文,如果需要加密需要在shiroconfig中设置加密方法,salt(盐), 以及迭代次数,推荐方式为md5加密)
@RequestMapping("/Login")
public String Login(String username,String password,Model model){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
//值得注意的是,此时的login会调用一个名为ModularRealmAuthenticator的方法,
//该方法会判断是否存在多个Realm会分别调用doSingleRealmAuthentication或者doMultiRealmAuthentication方法,
//这两个方法都会返回一个AuthenticationInfo类对象,
//该对象通常包含了数据库中通过用户id查询到的密文密码以及用户输入的明文密码以及加密方式
//(具体实现在Realm中显示实现)方法源码放在下面
// 其中的登录操作是交由shiro来进行的,密码并不需要我们来验证,
//由shiro来做这件事(个人认为体现了shiro的安全性)
subject.login(token);
//通过subject获取当前用户数据
Subject currentSubject = SecurityUtils.getSubject();
Session session = currentSubject.getSession();
User user = (User)currentSubject.getPrincipal();
session.setAttribute("loginUser",user);
return "index";
} catch (UnknownAccountException e) {
model.addAttribute("msg","用户名错误");
return "Login";
} catch (IncorrectCredentialsException e){
model.addAttribute("msg","密码错误");
return "Login";
}
}
在ModularRealmAuthenticator类中的方法中可以很清晰地看到,调用了realm中的getAuthenticationInfo方法,该方法获得了从前端传递的用户名和密码,
包装进了info对象中,具体包装过程由自己定义
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
/**
* Performs the multi-realm authentication attempt by calling back to a {@link AuthenticationStrategy} object
* as each realm is consulted for {@code AuthenticationInfo} for the specified {@code token}.
*
* @param realms the multiple realms configured on this Authenticator instance.
* @param token the submitted AuthenticationToken representing the subject's (user's) log-in principals and credentials.
* @return an aggregated AuthenticationInfo instance representing account data across all the successfully
* consulted realms.
*/
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
for (Realm realm : realms) {
try {
aggregate = strategy.beforeAttempt(realm, token, aggregate);
} catch (ShortCircuitIterationException shortCircuitSignal) {
// Break from continuing with subsequnet realms on receiving
// short circuit signal from strategy
break;
}
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
Throwable t = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, t);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
以下是在Realm中自己定义的认证方法,在密码或用户名错误时会抛出对应异常,如果账户被锁定也会拒绝访问并抛出异常。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken userToken = (UsernamePasswordToken)token;
QueryWrapper<User> qw = new QueryWrapper();
String userName = userToken.getUsername();
qw.select("pwd" , "id" , "locked" , "name").eq("user_id" , userName);
User user = userMapperPlus.selectOne(qw);
if(user == null) {
throw new UnknownAccountException();
}else if(user.getLocked().equals(1)){
throw new LockedAccountException();
}
System.out.println(token.getPrincipal() + user.getName());
return new SimpleAuthenticationInfo(token.getPrincipal() , user.getPwd(), ByteSource.Util.bytes(userToken.getUsername() + user.getName()), userName);
}
如果密码不正确会抛回controller层执行相应代码,如果验证成功则正常返回页面
(注意,这里的principal,也就是返回值方法的第一个参数是object类,
用来传递用户的相关信息,此信息可以通过subject对象获取)
subject获取方式为
Subject currentSubject = SecurityUtils.getSubject();
通过getPrincipal()方法可以获取
以下是授权时所要做的事(也就是perms级别)
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获取当前用户信息
Subject subject = SecurityUtils.getSubject();
User currentUser = (User)subject.getPrincipal();
//进行授权
info.addStringPermission(currentUser.getPerms());
System.out.println("执行了授权认证");
return info;
}
bean.setUnauthorizedUrl("/noAuth");