一、什么是shiro?
1、Shiro是一个安全框架,可以进行角色、权限管理。
Shiro主要功能如下:
Authentication(认证):用户身份识别,通常被称为用户“登录”
Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
(记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。)
2、shiro主要的类
①Subject:当前用户,Subject可以是一个人,也可以是第三方服务
②SecurityManager:管理所有Subject,可以配合内部安全组件。
③principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。
④credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
最常见的principals和credentials组合就是用户名/密码了。
⑤Realms:用于进行权限信息的验证,需要自己实现。
Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。
⑥在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。
我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
⑦SimpleHash,可以通过特定算法(比如md5)配合盐值salt,对密码进行多次加密。
二、接下来开始整合shiro
1、引入所需要的jar包依赖
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
2、创建实体类
User.java
import lombok.Data;
@Data
public class User {
private Integer userId;
private String userName;
private String password;
private String status;
}
Role.java
import lombok.Data;
@Data
public class Role {
private Integer roleId;
private String roleName;
private String roleNote;
}
UserRole.java
import lombok.Data;
@Data
public class UserRole {
private Integer userRoleId;
private Integer userId;
private Integer roleId;
private String userRoleNote;
}
RolePermission.java
import lombok.Data;
@Data
public class RolePermission {
private Integer rolePermissionId;
private Integer roleId;
private String permissionName;
}
3、创建shiro的realm
MyShiroRealm.java
import com.vtech.packinglist.common.GlobalConstant;
import com.vtech.packinglist.exception.UsersServiceException;
import com.vtech.packinglist.mapper.UserMapper;
import com.vtech.packinglist.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import javax.annotation.Resource;
import java.util.Set;
@Slf4j
public class MyShiroRealm extends AuthorizingRealm {
@Resource
private UserMapper userMapper;
/**
* 权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User users = (User) SecurityUtils.getSubject().getPrincipal();
Set<String> role = userMapper.selectRoleNameByUserId(users.getUserId());
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(role);
return simpleAuthorizationInfo;
}
/**
* 身份认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 将AuthenticationToken强转为UsernamePasswordToken对象
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
// 获得从表单传过来的用户名
String username = upToken.getUsername();
String pwd = String.valueOf(upToken.getPassword());
Subject subject = SecurityUtils.getSubject();
User user = null;
try {
User users = userMapper.findUserStatusByUserName(username);
// 如果用户不存在,抛此异常
if (null == users) {
throw new UnknownAccountException("Can not find this user!");
}
user = userMapper.findUserByUserAndPwd(username, pwd);
if (user == null) {
throw new IncorrectCredentialsException();
}
subject.getSession().setAttribute(GlobalConstant.SESSTION_AUTH_USERINFO, SecurityUtils.getSubject().getPrincipal());
subject.getSession().setAttribute(GlobalConstant.USER_NAME, user.getUserName());
} catch (UsersServiceException e) {
log.error(e.getMessage(),e);
}
// 创建SimpleAuthenticationInfo对象,并且把username和password等信息封装到里面
// 用户密码的比对是Shiro帮我们完成的
return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
}
}
4、创建shiro配置类
shiroConfig.java
import com.vtech.packinglist.common.LocalFromAuthenticationFilter;
import com.vtech.packinglist.realm.MyShiroRealm;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro 内置过滤器,可以实现权限相关的拦截器
* 常用的过滤器:
* anon:无需认证(登录)可以访问
* authc:必须认证才可以访问
* user:如果使用rememberMe的功能可以直接访问
* perms:该资源必须得到资源权限才可以访问
* role:该资源必须得到角色的权限才可以访问
*/
@Slf4j
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置login URL
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/booking/index.html");
// 未授权的页面
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized.html");
HashMap<String, Filter> hashMap = new HashMap<String, Filter>();
hashMap.put("shiroLoginFilter", shiroLoginFilter());
shiroFilterFactoryBean.setFilters(hashMap);
//添加Shiro内置过滤器
// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//静态资源
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/font/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/plugins/**", "anon");
filterChainDefinitionMap.put("/scripts/**", "anon");
// 设置登录的URL为匿名访问,因为一开始没有用户验证
filterChainDefinitionMap.put("/login.do", "anon");
// 退出系统的过滤器
filterChainDefinitionMap.put("/logout.do", "logout");
// 现在资源的角色
//filterChainDefinitionMap.put("/hello/welcome.do", "roles[test]");
// filterChainDefinitionMap.put("/user.html", "roles[user]");
//其余接口一律拦截
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
log.info("Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
/**
* 盐
* 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* 所以我们需要修改下doGetAuthenticationInfo中的代码; )
*/
/*@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(1024);// 散列的次数,比如散列两次,相当于md5(md5(""));
return hashedCredentialsMatcher;
}*/
/**
* 自定义身份认证 realm;
* 必须写这个类,并加上 @Bean 注解,目的是注入 MyShiroRealm,
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
//需要盐时才添加(加密)
//myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
/**
* 注入 securityManager
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 注入自定义的realm;
securityManager.setRealm(myShiroRealm());
// 注入缓存管理器;
//securityManager.setCacheManager(ehCacheManager());
return securityManager;
}
/**
* 开启shiro aop注解支持 使用代理方式;所以需要开启代码支持;
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
*/
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
/**
* 处理ajax请求
*/
@Bean(name = "shiroLoginFilter")
public LocalFromAuthenticationFilter shiroLoginFilter() {
LocalFromAuthenticationFilter shiroLoginFilter = new LocalFromAuthenticationFilter();
return shiroLoginFilter;
}
/**
* shiro缓存管理器;
* 需要注入对应的其它的实体类中-->安全管理器:securityManager可见securityManager是整个shiro的核心;
*/
/* @Bean
public EhCacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}*/
}
5、创建Mapper
UserMapper.java
public interface UserMapper {
User findUserByUserAndPwd(@Param("userName") String userName, @Param("password") String password);
User findUserStatusByUserName(String userName);
Set<String> selectRoleNameByUserId(Integer userId);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.vtech.packinglist.mapper.UserMapper" >
<resultMap id="BaseResultMap" type="com.vtech.packinglist.pojo.User" >
<id column="USER_ID" property="userId" jdbcType="INTEGER" />
<result column="USER_NAME" property="userName" jdbcType="NVARCHAR" />
<result column="PASSWORD" property="password" jdbcType="NVARCHAR" />
<result column="STATUS" property="status" jdbcType="NVARCHAR" />
</resultMap>
<sql id="Base_Column_List" >
USER_ID, USER_NAME, PASSWORD, STATUS
</sql>
<select id="findUserByUserAndPwd" resultMap="BaseResultMap">
select * from TS_USER where user_name = #{userName} and password= #{password} and status='ACTIVE'
</select>
<select id="findUserStatusByUserName" resultMap="BaseResultMap">
select *
from TS_USER
where 1=1 and user_name = #{userName}
and status = 'ACTIVE'
</select>
<select id="selectRoleNameByUserId" resultType="java.lang.String">
select USER_ROLES.ROLE_NAME from TS_USER
left join USER_ROLES on users.USER_ID = USER_ROLES.USER_ID
where USERS.USER_ID = #{userId}
</select>
</mapper>
6、创建LoginController.java
LoginController.java
@RestController
@Slf4j
public class LoginController {
@ApiOperation(value="登录界面", notes="")
@RequestMapping(value = "/login.html",method = RequestMethod.GET)
public ModelAndView test() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("common/login");
return modelAndView;
}
@ApiOperation(value="404 not found页面", notes="")
@RequestMapping(value = "/404.html",method = RequestMethod.GET)
public ModelAndView page404() {
return new ModelAndView("/hui/404");
}
@ApiOperation(value="没有权限页面", notes="")
@RequestMapping(value = "/unauthorized.html",method = RequestMethod.GET)
public ModelAndView unauthorized(){
return new ModelAndView("/hui/_blank");
}
@ApiOperation(value = "系统主页", notes = "")
@RequestMapping(value = "/index",method = RequestMethod.GET)
public ModelAndView index(){
return new ModelAndView("/common/index");
}
@RequestMapping(value = "/home.html",method = RequestMethod.GET)
public ModelAndView home(){
return new ModelAndView("/common/home");
}
@LogAnno("请求登录")
@ApiOperation(value="请求登录", notes="登录操作")
@RequestMapping(value = "/login.do",method = RequestMethod.POST)
public JsonResult login(String username, String password) {
//判断账号和密码是否为空,若为空则返回提示信息
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
return new JsonResult(JsonResult.ERROR, "Please enter your account name or password!");
}
try {
// 从SecurityUtils里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 执行认证登陆
subject.login(token);
} catch (IncorrectCredentialsException e) {
return new JsonResult(JsonResult.ERROR, "The account or password is wrong!");
} catch (LockedAccountException e) {
return new JsonResult(JsonResult.ERROR, "Account is disable or does not exist!");
} catch (ConcurrentAccessException e) {
return new JsonResult(JsonResult.ERROR, "Login failed,please log in again!");
} catch (Exception e) {
e.printStackTrace();
return new JsonResult(JsonResult.ERROR, e.getMessage());
}
return new JsonResult(GlobalConstant.AUTHENTICATION_SUCCESS);
}
@ApiOperation(value="登出", notes="")
@LogAnno("退出登录")
@RequestMapping(value = "/logout.do",method = RequestMethod.GET)
public ModelAndView logout() {
return new ModelAndView("hui/login");
}
}
到这里就完成了整合shiro实现登录的功能,在shiroConig中设置拦截的url即可。