主体(subject)需要携带身份信息和凭证信息,shiro在认证时会将这些信息打包成一个令牌,进入到安全管理器中进行认证。
public class TestAuthenticator {
public static void main(String[] args) {
//1.创建安全管理器对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2.给安全管理器设置realm
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//3.SecurityUtils 给全局安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//4.关键对象 subject 主体
Subject subject = SecurityUtils.getSubject();
//5.创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen","123");
try{
System.out.println("认证状态: "+ subject.isAuthenticated());
subject.login(token);//用户认证
System.out.println("认证状态: "+ subject.isAuthenticated());
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("认证失败: 用户名不存在~");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("认证失败: 密码错误~");
}
}
}
源码分析:
① subject.login(token)实际上底层是由securityManager进行认证的:
public void login(AuthenticationToken token) throws AuthenticationException {
this.clearRunAsIdentitiesInternal();
//底层是由securityManager进行认证的,this传入的是安全管理器的实现类,token是用户信息和认证信息
Subject subject = this.securityManager.login(this, token);
//......
}
}
② DefaultSecurityManager类中在login()方法内部,会调用authticate(token)方法进行认证,认证后返回认证信息:
public Subject login(Subject subject, AuthenticationToken token)throws AuthenticationException{
//封装了认证信息
AuthenticationInfo info;
try {
//进项认证,返回认证信息
info = authenticate(token);
} catch (AuthenticationException ae) {
//.....
}
return loggedIn;
}
③ AuthenticatingSecurityManager类,即调用的父类的中的authenticate(token)方法:
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
//调用当前这个类中的anthenticator这个方法中的authenticate(token)方法
return this.authenticator.authenticate(token);
}
④ AbstractAuthenticator类,在这个类中调用方法authenticate(token)方法:
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
//认证信息
AuthenticationInfo info;
try {
//执行认证
info = doAuthenticate(token);
}
//.....
throw ae;
}
⑤ 判断是否配置realm,然后继续认证:
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
//断言,是否配置realm
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
//如果只配置了一个realm,进行认证
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
⑥ 调用realm.getAuthenticationInfo(token)
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
AuthenticationInfo info = realm.getAuthenticationInfo(token);
return info;
}
⑦ getAuthenticationInfo( token)方法:先从缓存中获取认证信息,先进行用户名的认证
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
}
//.....
return info;
}
⑧ 在SimpleAccountRealm类中的doGetAuthenticationInfo()方法:这个方法只完成了用户名的认证
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//将AuthenticationToken强转为UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//通过用户名获取用户信息
SimpleAccount account = getUser(upToken.getUsername());
return account;
}
⑨ getAuthenticationInfo( token)方法:继续进行用户密码的认证
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//如果用户信息为null,就去认证用户名是否正确
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
}
if (info != null) {
//如果用户信息不为null,就去认证密码是否正确
assertCredentialsMatch(token, info);
}
return info;
}
总结:分析到这儿就结束了,就是说如果我们想自定义Realm,就需要重写doGetAuthenticationInfo()方法换成自己去读数据库用户信息即可,密码不需要我们去校验,密码是在用户信息校验之后自己去校验的:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//将AuthenticationToken强转为UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//通过用户名获取用户信息
SimpleAccount account = getUser(upToken.getUsername());
return account;
}
真正实现认证的类就是SimpleAccountRealm,看下这个类的源码,可以得知,这个类继承自AuthorizingRealm,因此如果我们想自定义realm也需要继承这个类AuthorizingRealm
public class SimpleAccountRealm extends AuthorizingRealm {
// .....
//实现认证的方法
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
// 实现授权的方法
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = getUsername(principals);
USERS_LOCK.readLock().lock();
try {
return this.users.get(username);
} finally {
USERS_LOCK.readLock().unlock();
}
}
}
自定义Realm的目的是将认证/授权的数据源转为数据库:
public class CustomerRealm extends AuthorizingRealm {
//授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从token中获取用户名
String principal = (String) token.getPrincipal();
if("xiaochen".equals(principal)){
//数据库中的身份信息、凭证信息、realm的名字
return new SimpleAuthenticationInfo(principal,"123",this.getName());
}
return null;
}
}
使用自定义realm进行认证:
public class TestAuthenticatorCusttomerRealm {
public static void main(String[] args) {
//创建securityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//IniRealm realm = new IniRealm("classpath:shiro.ini");
//设置为自定义realm获取认证数据
defaultSecurityManager.setRealm(new CustomerRealm());
//将安装工具类中设置默认安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体对象
Subject subject = SecurityUtils.getSubject();
//创建token令牌,交给安全管理器去进行认证(查询数据库信息并比较是否相同)
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");
try {
subject.login(token);//用户登录
System.out.println("登录成功~~");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!!");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误!!!");
}
}
}
实际应用是将盐和散列后的值存在数据库中,自动realm从数据库取出盐和加密后的值由shiro完成密码校验。
public class TestShiroMD5 {
public static void main(String[] args) {
//使用md5
Md5Hash md5Hash = new Md5Hash("123");
System.out.println(md5Hash.toHex());
//使用MD5 + salt处理
Md5Hash md5Hash1 = new Md5Hash("123", "X0*7ps");
System.out.println(md5Hash1.toHex());
//使用md5 + salt + hash散列
Md5Hash md5Hash2 = new Md5Hash("123", "X0*7ps", 1024);
System.out.println(md5Hash2.toHex());
}
}
使用密码加盐自定义Realm:
public class CustomerRealm extends AuthorizingRealm {
//授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
if("xiaochen".equals(principal)){
//数据库中加密加盐后使用md5算法后的密码
String password = "3c88b338102c1a343bcb88cd3878758e";
//盐
String salt = "Q4F%";
return new SimpleAuthenticationInfo(principal,password,
ByteSource.Util.bytes(salt),this.getName());
}
return null;
}
}
使用密码加盐自定义Realm进行认证:
public class TestAuthenticatorCusttomerRealm {
public static void main(String[] args) {
//创建securityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置为自定义realm获取认证数据
CustomerRealm customerRealm = new CustomerRealm();
//设置md5加密
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");
//如果注册时用户数据库中的密码散列了1024次,就需要告诉shiro也做相同的处理
credentialsMatcher.setHashIterations(1024);//设置散列次数
customerRealm.setCredentialsMatcher(credentialsMatcher);
defaultSecurityManager.setRealm(customerRealm);
//将安装工具类中设置默认安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体对象
Subject subject = SecurityUtils.getSubject();
//创建token令牌,realm使用了MD5加密算法,因此会对用户输入的这个密码使用md5算法加密,然后去认证
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");
try {
subject.login(token);//用户登录
System.out.println("登录成功~~");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!!");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误!!!");
}
}
}
授权可简单理解为who对what(which)进行How操作:
Who,即主体(Subject)
,What,即资源(Resource)
,How,权限/许可(Permission)
授权:认证通过后就会进行授权,通过用户名到数据库中查询用户的角色和权限信息然后返回判断
public class CustomerRealm extends AuthorizingRealm {
//授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//拿到用户名
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("primaryPrincipal = " + primaryPrincipal);
//根据用户名到数据库中获取该用户具有的角色信息和权限信息,获取后返回
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//到数据库中获取该用户对应的角色信息
simpleAuthorizationInfo.addRole("admin");
//到数据库中获取该用户对应的权限信息
simpleAuthorizationInfo.addStringPermission("user:update:*");
simpleAuthorizationInfo.addStringPermission("product:*:*");
//返回权限和角色信息
return simpleAuthorizationInfo;
}
//认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
if("xiaochen".equals(principal)){
String password = "3c88b338102c1a343bcb88cd3878758e";
String salt = "Q4F%";
return new SimpleAuthenticationInfo(principal,password,
ByteSource.Util.bytes(salt),this.getName());
}
return null;
}
}
权限和角色判断:
public class TestAuthenticatorCusttomerRealm {
public static void main(String[] args) {
//创建securityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置为自定义realm获取认证数据
CustomerRealm customerRealm = new CustomerRealm();
//设置md5加密
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");
credentialsMatcher.setHashIterations(1024);//设置散列次数
customerRealm.setCredentialsMatcher(credentialsMatcher);
defaultSecurityManager.setRealm(customerRealm);
//将安装工具类中设置默认安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体对象
Subject subject = SecurityUtils.getSubject();
//创建token令牌
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");
try {
subject.login(token);//用户登录
System.out.println("登录成功~~");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!!");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误!!!");
}
//认证通过
if(subject.isAuthenticated()){
//基于角色权限管理
boolean admin = subject.hasRole("admin");
System.out.println(admin);
//是否有某个权限
boolean permitted = subject.isPermitted("product:create:001");
System.out.println(permitted);
//基于多角色权限控制
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "super")));
//是否具有其中一个角色
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "super", "user"));
for (boolean aBoolean : booleans) {
System.out.println(aBoolean);
}
System.out.println("==============================================");
//基于权限字符串的访问控制 资源标识符:操作:资源类型
System.out.println("权限:"+subject.isPermitted("user:update:01"));
System.out.println("权限:"+subject.isPermitted("product:create:02"));
//分别具有那些权限
boolean[] permitted = subject.isPermitted("user:*:01", "order:*:10");
for (boolean b : permitted) {
System.out.println(b);
}
//同时具有哪些权限
boolean permittedAll = subject.isPermittedAll("user:*:01", "product:create:01");
System.out.println(permittedAll);
}
}
}
@Service("userService")
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDAO;
@Override
public List<Perms> findPermsByRoleId(String id) {
return userDAO.findPermsByRoleId(id);
}
@Override
public User findRolesByUserName(String username) {
return userDAO.findRolesByUserName(username);
}
@Override
public User findByUserName(String username) {
return userDAO.findByUserName(username);
}
//用户注册时使用的MD5算法对密码使用密文保存
@Override
public void register(User user) {
//处理业务调用dao
//1.生成随机盐
String salt = SaltUtils.getSalt(8);
//2.将随机盐保存到数据
user.setSalt(salt);
//3.明文密码进行md5 + salt + hash散列
Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
user.setPassword(md5Hash.toHex());
userDAO.save(user);
}
}
@Controller
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
/**
* 用户注册
*/
@RequestMapping("register")
public String register(User user) {
try {
userService.register(user);
return "redirect:/login.jsp";
}catch (Exception e){
e.printStackTrace();
return "redirect:/register.jsp";
}
}
/**
* 用来处理身份认证:
* 将用户输入的用户名和密码封装成UsernamePasswordToken后交给shiro的安全管理器进行认证
*/
@RequestMapping("login")
public String login(String username, String password,String code,HttpSession session) {
//比较验证码
String codes = (String) session.getAttribute("code");
try {
if (codes.equalsIgnoreCase(code)){
//获取主体对象
Subject subject = SecurityUtils.getSubject();
//交给shiro的安全管理器进行认证
subject.login(new UsernamePasswordToken(username, password));
return "redirect:/index.jsp";
}else{
throw new RuntimeException("验证码错误!");
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误!");
}catch (Exception e){
e.printStackTrace();
System.out.println(e.getMessage());
}
return "redirect:/login.jsp";
}
/**
* 退出登录
*/
@RequestMapping("logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();//退出用户
return "redirect:/login.jsp";
}
/**
* 验证码方法
*/
@RequestMapping("getImage")
public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
//生成验证码
String code = VerifyCodeUtils.generateVerifyCode(4);
//验证码放入session
session.setAttribute("code",code);
//验证码存入图片
ServletOutputStream os = response.getOutputStream();
response.setContentType("image/png");
VerifyCodeUtils.outputImage(220,60,os,code);
}
}
/**
* 用来整合shiro框架相关的配置类
*/
@Configuration
public class ShiroConfig {
//1.创建shiroFilter:负责拦截所有请求
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
Map<String,String> map = new HashMap<String,String>();
//anon:配置的请求路径放行
map.put("/user/login","anon");
map.put("/user/register","anon");
map.put("/register.jsp","anon");
map.put("/user/getImage","anon");
//authc:请求这个资源需要认证和授权
map.put("/**","authc");
//默认认证界面路径
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//2.创建安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//3.创建自定义realm
@Bean
public Realm getRealm(){
//需要配置密码的认证方式,不然那就还是用户用户输入的明文,用户输入明文后对这个明文做处理
//处理的方式需要和用户注册时使用的方式相同
CustomerRealm customerRealm = new CustomerRealm();
//修改凭证校验匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置加密算法为md5
credentialsMatcher.setHashAlgorithmName("MD5");
//设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
}
注意:这里先不考虑授权
因为我们注册的时候用户密码使用的MD5+salt的方式,因此在认证的时候我们就需要指定用户密码的认证方式,
//自定义realm
public class CustomerRealm extends AuthorizingRealm {
/**
* 用户认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从token解析出用户输入的身份信息
String principal = (String) token.getPrincipal();
//在工厂中获取service对象
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
//到数据库中查询用户user
User user = userService.findByUserName(principal);
//如果user不为null,就返回从数据库中查询出的用户名和密码以及盐等信息
if(!ObjectUtils.isEmpty(user)){
return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),
new MyByteSource(user.getSalt()),
this.getName());
}
return null;
}
/**
* 用户授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
User user = userService.findRolesByUserName(primaryPrincipal);
//授权角色信息
if(!CollectionUtils.isEmpty(user.getRoles())){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//根据user到数据库中查询用户的角色信息
//遍历每个角色,获取每个角色对应的权限信息
user.getRoles().forEach(role->{
//将角色信息添加到simpleAuthorizationInfo中
simpleAuthorizationInfo.addRole(role.getName());
//根据roleId获取用户的每个角色对应的权限信息
List<Perms> perms = userService.findPermsByRoleId(role.getId());
//遍历权限列表
if(!CollectionUtils.isEmpty(perms)){
perms.forEach(perm->{
//将权每个权限添加到simpleAuthorizationInfo中
simpleAuthorizationInfo.addStringPermission(perm.getName());
});
}
});
//返回用户的角色和权限信息
return simpleAuthorizationInfo;
}
return null;
}
}
@Controller
@RequestMapping("order")
public class OrderController {
@RequiresRoles(value={"admin","user"})//用来判断角色 同时具有 admin user
@RequiresPermissions("user:update:01") //用来判断权限字符串
@RequestMapping("save")
public String save(){
System.out.println("进入方法");
return "redirect:/index.jsp";
}
}