Shiro安全认证框架
Shiro 概述
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架,使用shiro就可以非常快速的完成认证、授权等功能的开发,降低系统成本。
Shiro 认证流程
具体流程分析如下:
- 首先调用Subject.login(token)进行登录,其会自动委托给Security Manager,调用之前必须通过SecurityUtils. setSecurityManager()设置;
- SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证;
- Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现;
- Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证;
- Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
Jwt
-
什么是JWT
JWT(Json Web Token),是一种工具,格式为XXXX.XXXX.XXXX的字符串,JWT以一种安全的方式在用户和服务器之间传递存放在JWT中的不敏感信息。 -
为什么要用JWT
设想这样一个场景,在我们登录一个网站之后,再把网页或者浏览器关闭,下一次打开网页的时候可能显示的还是登录的状态,不需要再次进行登录操作,通过JWT就可以实现这样一个用户认证的功能。当然使用Session可以实现这个功能,但是使用Session的同时也会增加服务器的存储压力,而JWT是将存储的压力分布到各个客户端机器上,从而减轻服务器的压力。 -
JWT长什么样子?
JWT由3个子字符串组成,分别为Header,Payload以及Signature,结合JWT的格式即:Header.Payload.Signature。(Claim是描述Json的信息的一个Json,将Claim转码之后生成Payload)。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODY1MTA3MTIsInVzZXJuYW1lIjoiZGV2aW4ifQ.9MHYCpJCfKBbaDcJV4NUneL8OM9BdNaNrLgGW3Nznxc
Shiro + Jwt 应用实践
- pom.xml 添加 Shiro 和 Jwt 依赖
<!-- 整合shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- jwt依赖 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
- 编写 JwtUtil 工具类
@Component
public class JwtUtil {
/**
* 失效时间
*/
private static final long EXPIRE_TIME = 5 * 60 * 1000;
/**
* 生成token 5分钟后过期
* @author 药岩
* @date 2020/2/10
* @param * @param username
* @return java.lang.String
*/
public static String sign(String username, String secret){
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
return JWT.create()
//payload 载荷
.withClaim("username", username)
//设置过期时间
.withExpiresAt(date)
//创建一个新的JWT,并使用给定的算法进行标记
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
return null;
}
}
/**
* 校验token是否正确
* @author 药岩
* @date 2020/2/10
* @param * @param token
* @param userName
* @return boolean
*/
public static boolean validateToken(String token, String userName, String secret){
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
//在token中附带了username信息
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", userName)
.build();
//验证token
verifier.verify(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 获取token中的信息无需secretKey也能获取
* @author 药岩
* @date 2020/2/10
* @param * @param token
* @return java.lang.String
*/
public static String getUserName(String token){
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
- 编写 JwtToken 实现 AuthenticationToken 接口
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 2334863906091691445L;
/**
* 秘钥
*/
private String token;
public JwtToken(String token){
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
- 自定义 shiro 的过滤器,不要使用 shiro 默认的过滤器
public class JwtShiroFilter extends BasicHttpAuthenticationFilter {
private final Logger log = LoggerFactory.getLogger(JwtShiroFilter.class);
@Autowired
private RedisUtil redisUtil;
/**
* 如果带有 token,则对 token 进行检查,否则直接通过
* @param request
* @param response
* @param mappedValue
* @return
* @throws UnauthorizedException
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
//非法请求
responseError(response, e.getMessage());
}
}
//如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
return true;
}
/**
* 判断用户是否想要登入
* @author 药岩
* @date 2020/2/10
* @param * @param request
* @param response
* @return boolean
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
String authorization = httpServletRequest.getHeader("Authorization");
return authorization != null;
}
/**
* 执行登陆操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception{
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
String authorization = httpServletRequest.getHeader("Authorization");
JwtToken token = new JwtToken(authorization);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 将非法请求跳转到 /401
*/
private void responseError(ServletResponse response, String message) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//设置编码,否则中文字符在重定向时会变为空字符串
message = URLEncoder.encode(message, "UTF-8");
httpServletResponse.sendRedirect("/unauthorized/" + message);
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
- 自定义 JwtShiroRealm 继承 AuthorizingRealm 实现认证与授权
/**
* 监视授权信息
* @ClassName ShiroUserRealm
* @Author 药岩
* @Date 2020/2/10
* Version 1.0
*/
public class JwtShiroRealm extends AuthorizingRealm {
@Autowired
private AuthorityDao authorityDao;
@Autowired
private LoginDao loginDao;
@Autowired
private LoginServer loginServer;
public void setLoginServer(LoginServer loginServer){
this.loginServer = loginServer;
}
/**
* 判断token是否事我们的这个jwttoekn
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 认证
* @author 药岩
* @date 2020/2/10
* @param * @param authcToken
* @return org.apache.shiro.authc.AuthenticationInfo
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
//0. token中存储着输入的用户名
String token = (String) auth.getCredentials();
//1. token解密获取username
String userName = JwtUtil.getUserName(token);
if (userName == null){
throw new AuthenticationException("token令牌有误");
}
//2.根据用户名去数据库查找是否存在该用户
Users users = loginDao.selectUserData(userName);
if(users == null){
throw new AuthenticationException("用户不存在");
}
//3.判断token令牌是否有误
if (!JwtUtil.validateToken(token, userName, users.getPassword())){
throw new AuthenticationException("token令牌过期失效");
}
return new SimpleAuthenticationInfo(token, token, "jwtRealm");
}
/**
* 授权
* @author 药岩
* @date 2020/2/10
* @param * @param principals
* @return org.apache.shiro.authz.AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//1.获取用户信息
String userName = JwtUtil.getUserName(principals.toString());
//2.根据用户名查找该用户
Users users = loginDao.selectUserData(userName);
//2.根据用户信息-->角色信息-->获取资源的访问权陿
List<String> menus = authorityDao.findUserPermissions(users.getId());
//3.根据用户信息直接获取访问权限
List<String> userMenu = authorityDao.findUserMenuPermissions(users.getId());
//查询角色
List<String> role = authorityDao.findRoleByUserId(users.getId());
List<String> list = new ArrayList<>();
if(menus!=null){
list.addAll(menus);
}
if(userMenu!=null){
list.addAll(userMenu);
}
//4.去重权限
Set<String> set=new HashSet<>(list);
Set<String> setRole = new HashSet<>(role);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(set);
info.setRoles(setRole);
return info;
}
}
- 编写 Shiro 的配置类 ShiroConfig
/**
* Shiro 的配置类
* @ClassName ShiroConfig
* @Author 药岩
* @Date 2020/2/10
* Version 1.0
*/
@Configuration
public class ShiroConfig {
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @author 药岩
* @date 2020/2/10
* @param * @param
* @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
*/
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
/**
* 开启aop注解支持
* @author 药岩
* @date 2020/2/10
* @param * @param securityManager
* @return org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 自定义shiro
* @author 药岩
* @date 2020/2/10
* @param * @param
* @return com.app.common.ShiroUserRealm
*/
@Bean("jwtRealm")
public JwtShiroRealm shiroUserRealm(){
JwtShiroRealm userRealm = new JwtShiroRealm();
return userRealm;
}
/**
* 注入SecurityManager
* 配置shiro安全管理器,是shiro框架的核心安全管理器
* @author 药岩
* @date 2020/2/10
* @param * @param
* @return org.apache.shiro.mgt.SecurityManager
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(shiroUserRealm());
//关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
return defaultWebSecurityManager;
}
/**
* shiroFilter 的工厂配置
* @author 药岩
* @date 2020/2/10
* @param * @param securityManager
* @return org.apache.shiro.spring.web.ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//添加自定义shiro的拦截器
Map<String, Filter> jwtFilter = new HashMap<>();
jwtFilter.put("jwt", new JwtShiroFilter());
shiroFilterFactoryBean.setFilters(jwtFilter);
//必须设置 securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置无权限时跳转的URL
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized/无权限");
//自定义URL
Map<String, String> map = new HashMap<>();
map.put("/user/doLogin", "anon");
map.put("/user/logout", "logout");
map.put("/swagger-ui.html", "anon");
map.put("/swagger-resources/**", "anon");
map.put("/unauthorized/**", "anon");
map.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
}
- 添加全局异常处理类
/**
* 全局异常处理
* @ClassName GlobalExceptionHandler
* @Author 药岩
* @Date 2020/4/10
* Version 1.0
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理shiro抛出的异常
* @author 药岩
* @date 2020/2/10
* @param * @param e
* @return com.app.common.CommonResult
*/
@ExceptionHandler(ShiroException.class)
public CommonResult handleShiroException(Exception e){
log.error("shiro error[{}]", e.getMessage());
return CommonResult.failed(401, "您没有权限访问", new int[]{});
}
@ExceptionHandler(Exception.class)
public CommonResult handleException(HttpServletRequest request, Throwable e){
log.error("系统访问异常error[{}]", e.getMessage());
return CommonResult.failed(getStatus(request).value(), "访问出错,无法访问:" + e.getMessage(), new int[]{});
}
public HttpStatus getStatus(HttpServletRequest request){
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
if (statusCode == null){
return HttpStatus.INTERNAL_SERVER_ERROR;
}
return HttpStatus.valueOf(statusCode);
}
}
- controller 层用户登入认证
/**
* 用户登入验证
* @ClassName LoginController
* @Author 药岩
* @Date 2020/2/10
* Version 1.0
*/
@RestController
@RequestMapping(value = "/user")
@Api(tags = "LoginController", description = "用户登入验证")
@Slf4j
public class LoginController {
@Autowired
private LoginServer loginServer;
@Autowired
private RedisUtil redisUtil;
/**
* 用于用户登入验证
* @param
* @param
* @return
*/
@PostMapping(value = "/doLogin")
public CommonResult login(@RequestBody Users usersLogin,
@RequestParam(required=true,value="checked",defaultValue="") String checked, HttpServletResponse response, HttpServletRequest request) throws UnauthorizedException {
//通过用户名
Users users = loginServer.selectUserData(usersLogin.getLoginName());
if (users == null){
return CommonResult.failed("用户不存在", new Object[]{});
}
usersLogin.setPassword(DigestUtils.md5DigestAsHex(usersLogin.getPassword().getBytes()));
if (users.getPassword().equals(usersLogin.getPassword())){
String token = JwtUtil.sign(usersLogin.getLoginName(), usersLogin.getPassword());
response.setHeader("Authorization", token);
log.info("用户[{}]登入成功,生成token[{}]", users.getLoginName(), token);
return CommonResult.success(token, "登入成功");
}else {
return CommonResult.failed("用户名密码错误", new Object[]{});
}
}
/**
* 登出操作
*/
@RequestMapping("logout")
public CommonResult logout(HttpServletRequest request){
SecurityUtils.getSubject().logout();
redisUtil.del(request.getHeader("Authorization"));
return CommonResult.success(new Object[]{}, "登出成功");
}
}
- Postman 测试
用户登入
携带 token 访问接口资源