这个demo是基于springboot项目的。
名词介绍:
Shiro
主要分为 安全认证 和 接口授权 两个部分,其中的核心组件为 Subject、 SecurityManager、 Realms,公共部分 Shiro 都已经为我们封装好了,我们只需要按照一定的规则去编写响应的代码即可…
Subject
表示主体,将用户的概念理解为当前操作的主体,因为它即可以是一个通过浏览器请求的用户,也可能是一个运行的程序,外部应用与 Subject 进行交互,记录当前操作用户。Subject 代表了当前用户的安全操作
SecurityManager
则管理所有用户的安全操作。
SecurityManager
即安全管理器,对所有的 Subject 进行安全管理,并通过它来提供安全管理的各种服务(认证、授权等)
Realm
充当了应用与数据安全间的 桥梁 或 连接器。当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。
项目结构
org.springframework.boot spring-boot-starter-test test junit junit 4.12 test org.springframework.boot spring-boot-starter-web org.projectlombok lombok true mysql mysql-connector-java runtime org.springframework.boot spring-boot-starter-jdbc com.baomidou mybatis-plus-boot-starter 3.4.2 org.freemarker freemarker 2.3.30 com.baomidou mybatis-plus-generator 3.4.1 org.apache.shiro shiro-spring 1.7.1 org.springframework.boot spring-boot-starter-aop org.crazycake shiro-redis 2.8.24 org.apache.commons commons-lang3 3.9 io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2
将用户信息交给redis管理
使用swagger2
import com.example.shirospringboot.realm.UserRealm;
import com.example.shirospringboot.shiro.MySessionManager;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.linkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Autowired
private RedisManager redisManager;
@Autowired
private RedisSessionDAO redisSessionDAO;
@Autowired
private RedisCacheManager redisCacheManager;
@Bean()
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
Map filterChainDefinitionMap =new linkedHashMap<>();
filterChainDefinitionMap.put("/login","anon");
filterChainDefinitionMap.put("/logout","anon");
//放行Swagger2页面,需要放行这些
filterChainDefinitionMap.put("/swagger-ui.html","anon");
filterChainDefinitionMap.put("/swagger
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
}
3.ShiroRedis配置类
application.properties
spring.redis.shiro.host=127.0.0.1 spring.redis.shiro.port=6379 spring.redis.shiro.timeout=5000 #没有密码写了会报错 #spring.redis.shiro.password=123456
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroRedisConfig {
@Value("${spring.redis.shiro.host}")
private String host;
@Value("${spring.redis.shiro.port}")
private int port;
@Value("${spring.redis.shiro.timeout}")
private int timeout;
// @Value("${spring.redis.shiro.password}")
// private String password;
@Bean
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setExpire(1800);// 配置缓存过期时间
redisManager.setTimeout(timeout);
// redisManager.setPassword(password);
return redisManager;
}
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
}
4.Swagger配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.documentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@EnableSwagger2
@Configuration
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(documentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.shirospringboot.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Shiro-Springboot Swagger2 APIs")
.description("shiro-springboot项目controller接口文档")
.version("1.0")
.build();
}
}
5.安全认证和权限验证的核心,自定义Realm
import com.example.shirospringboot.entity.User;
import com.example.shirospringboot.service.impl.UserServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.Set;
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserServiceImpl userService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
User user = userService.getUserByUserName(username);
if(user==null){
throw new AuthenticationException();
}
String credentials = user.getPassword();
ByteSource credentialsSalt = ByteSource.Util.bytes(user.getSalt());
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user.getUsername(), //用户名
credentials, //密码
credentialsSalt,
getName() //当前realm对象的name,调用父类的getName()方法即可
);
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("USER_SESSION", user);
return authenticationInfo;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Session session = SecurityUtils.getSubject().getSession();
System.out.println("授权方法获取用户:"+session.getAttribute("USER_SESSION"));
User user = (User) session.getAttribute("USER_SESSION");
Set roleSet = new HashSet<>();
Set permissionSet = new HashSet<>();
userService.getUserRoles(user.getUsername()).forEach(role -> roleSet.add(role.getName()));
userService.getUserPermissions(user.getUsername()).forEach(permission -> permissionSet.add(permission.getName()));
info.setRoles(roleSet);
info.setStringPermissions(permissionSet);
return info;
}
}
6.全局异常处理器
import com.example.shirospringboot.Result.Result;
import com.example.shirospringboot.Result.ResultUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
//无权限
@ExceptionHandler(value = UnauthorizedException.class)
public Result handler(UnauthorizedException e) {
return ResultUtil.NO_PERMISSION();
}
//身份过期
@ExceptionHandler(value = ExpiredCredentialsException.class)
public Result handler(ExpiredCredentialsException e) {
return ResultUtil.IDENTITY_EXPIRED();
}
//没有登陆
@ExceptionHandler(value = UnauthenticatedException.class)
public Result handler(UnauthenticatedException e) {
return ResultUtil.NOT_LOGGED_IN();
}
//密码错误
@ExceptionHandler(value = IncorrectCredentialsException.class)
public Result handler(IncorrectCredentialsException e) {
return ResultUtil.INCORRECT_PASSWORD();
}
//用户不存在
@ExceptionHandler(value = AuthenticationException.class)
public Result handler(AuthenticationException e) {
return ResultUtil.USER_NOT_FOUND();
}
//账号冻结
@ExceptionHandler(value = LockedAccountException.class)
public Result handler(LockedAccountException e) {
return ResultUtil.ACCOUNT_FREEZING();
}
}
7.因为现在的项目大多都是前后端分离的,所以我们需要实现自己的session管理
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
public class MySessionManager extends DefaultWebSessionManager {
private static final String TOKEN = "token";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(TOKEN);
//如果请求头中有 token 则其值为sessionId
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
//否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
8.控制器
import com.example.shirospringboot.Result.Result;
import com.example.shirospringboot.Result.ResultUtil;
import com.example.shirospringboot.entity.User;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
@Api(value="LoginController|登录控制器")
@RestController
@RequestMapping("/")
public class LoginController {
@ApiOperation(value = "用户登录接口", notes = "提供用户名和密码")
@PostMapping("/login")
public Result login(@ApiParam(name = "user",value = "登录用户",required = true) @RequestBody User user){
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
Subject subject = SecurityUtils.getSubject();
HashMap map = new HashMap<>();
try {
subject.login(token);
map.put("token", subject.getSession().getId());
} catch (IncorrectCredentialsException e) {
//密码错误
throw e;
} catch (LockedAccountException e) {
//冻结
throw e;
} catch (AuthenticationException e) {
//用户不存在
throw e;
} catch (Exception e) {
return ResultUtil.EXCEPTION_UNKNOWN();
}
System.out.println(user.getUsername()+"已登录");
return ResultUtil.SUCCESS("登陆成功",null);
}
@ApiOperation("用户登出接口")
@GetMapping("/logout")
public Result logout(){
SecurityUtils.getSubject().logout();
return ResultUtil.SUCCESS("已退出登录",null);
}
@ApiOperation("用户权限测试接口")
@GetMapping("/test")
@RequiresRoles(value = {"admin"})
@RequiresPermissions(value = {"order:query:zhujie"})
public Result test(){
return ResultUtil.SUCCESS("test",null);
}
}
常用注解
@RequiresGuest 代表无需认证即可访问,同理的就是 /path=anon
@RequiresAuthentication 需要认证,只要登录成功后就允许你操作
@RequiresPermissions 需要特定的权限,没有则抛出 AuthorizationException
@RequiresRoles 需要特定的橘色,没有则抛出 AuthorizationException
因为数据是模拟的,所以在登陆认证的时候,并没有通过数据库查用户信息,可以通过以下方式模拟加密后的密码:
String name="zhujie",password="123",salt= SaltUtil.getSalt();
System.out.println("salt:"+salt); //数据库salt字段
ByteSource saltByteSource = ByteSource.Util.bytes(salt);
String newPs = new SimpleHash("MD5", password, salt, 1024).toHex();
System.out.println("saltByteSource:"+saltByteSource);
System.out.println("密码:"+newPs); // 数据库password字段
}
UserService添加两个方法,便于调用。
public interface IUserService extends IService{ User getUserByUserName(String username); List getUserRoles(String username); List getUserPermissions(String username); } }
随机生成盐
import java.util.Random;
public class SaltUtil {
static char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!@#$%^&*()_+".toCharArray();
static int length=32;
public static String getSalt(int saltLength){
StringBuilder sb = new StringBuilder();
for(int i = 0; i < saltLength; i++){
char aChar = chars[new Random().nextInt(chars.length)];
sb.append(aChar);
}
return sb.toString();
}
public static String getSalt(){
return getSalt(length);
}
}
统一结果封装
import io.swagger.annotations.ApiModel;
import lombok.Data;
@Data
@ApiModel("统一封装返回对象")
public class Result {
private Integer code;
private String msg;
private T data;
}
public enum ResultEnum {
SUCCESS(200, "操作成功"),
EXCEPTION_NOT_LOGGED_IN(401, "未登录,请登录"),
EXCEPTION_USER_NOT_FOUND(401,"用户不存在"),
EXCEPTION_INCORRECT_PASSWORD(401,"密码错误"),
EXCEPTION_IDENTITY_EXPIRED(401, "身份已过期,请重新登录"),
EXCEPTION_ACCOUNT_FREEZING(401, "账号已被冻结"),
EXCEPTION_NO_PERMISSION(402, "无权限操作"),
EXCEPTION_UNKNOWN(400, "未知异常");
private Integer code;
private String msg;
ResultEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
import org.apache.commons.lang3.StringUtils;
public class ResultUtil {
public static Result SUCCESS(String message,Object object) {
Result result = new Result();
result.setCode(ResultEnum.SUCCESS.getCode());
if(StringUtils.isBlank(message)){
result.setMsg(ResultEnum.SUCCESS.getMsg());
}else{
result.setMsg(message);
}
result.setData(object);
return result;
}
public static Result SUCCESS() {
return SUCCESS(null,null);
}
public static Result NOT_LOGGED_IN() {
Result result = new Result();
result.setCode(ResultEnum.EXCEPTION_NOT_LOGGED_IN.getCode());
result.setMsg(ResultEnum.EXCEPTION_NOT_LOGGED_IN.getMsg());
return result;
}
public static Result USER_NOT_FOUND() {
Result result = new Result();
result.setCode(ResultEnum.EXCEPTION_USER_NOT_FOUND.getCode());
result.setMsg(ResultEnum.EXCEPTION_USER_NOT_FOUND.getMsg());
return result;
}
public static Result INCORRECT_PASSWORD() {
Result result = new Result();
result.setCode(ResultEnum.EXCEPTION_INCORRECT_PASSWORD.getCode());
result.setMsg(ResultEnum.EXCEPTION_INCORRECT_PASSWORD.getMsg());
return result;
}
public static Result IDENTITY_EXPIRED() {
Result result = new Result();
result.setCode(ResultEnum.EXCEPTION_IDENTITY_EXPIRED.getCode());
result.setMsg(ResultEnum.EXCEPTION_IDENTITY_EXPIRED.getMsg());
return result;
}
public static Result NO_PERMISSION() {
Result result = new Result();
result.setCode(ResultEnum.EXCEPTION_NO_PERMISSION.getCode());
result.setMsg(ResultEnum.EXCEPTION_NO_PERMISSION.getMsg());
return result;
}
public static Result ACCOUNT_FREEZING() {
Result result = new Result();
result.setCode(ResultEnum.EXCEPTION_ACCOUNT_FREEZING.getCode());
result.setMsg(ResultEnum.EXCEPTION_ACCOUNT_FREEZING.getMsg());
return result;
}
public static Result EXCEPTION_UNKNOWN() {
Result result = new Result();
result.setCode(ResultEnum.EXCEPTION_UNKNOWN.getCode());
result.setMsg(ResultEnum.EXCEPTION_UNKNOWN.getMsg());
return result;
}
}
10.出现的问题
问题1
添加以上依赖后,从缓存中读取用户信息会出现cast异常,所以不添加该热部署依赖即可
User user = (User) session.getAttribute("USER_SESSION");
问题2
放行Swagger2页面
filterChainDefinitionMap.put("/swagger-ui.html","anon");
filterChainDefinitionMap.put("/swagger/**","anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**","anon");
filterChainDefinitionMap.put("/v2/**","anon");
filterChainDefinitionMap.put("/static/**", "anon");



