发布时间:2024-12-19 09:01
通过扩展AccessControlFilter,HashedCredentialsMatcher完成了自定义身份校验器,访问控制过滤器等核心技术。
使用了全局业务异常,处理项目中可能出现的异常信息,并使用了枚举定义输出信息。
封装了通用的返回结果集,可在实际开发项目中,直接进行使用。
首先看图,先理解整合shiro之后如何实现前后端分离的项目
看完图我们会发现几个问题
为什么不使用shiro进行身份认证?
如何储存用户认证成功生成的 token ?
登录成功后,再次访问资源的时候,如何校验身份?
`<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>net.sf.ehcachegroupId>
<artifactId>ehcacheartifactId>
<version>2.10.4version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-ehcacheartifactId>
<version>1.4.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.7.0version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.7.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.16version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.62version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.4.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
<version>5.1.47version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<ehcache updateCheck=\"false\" dynamicConfig=\"false\">
<diskStore path=\"D:\\mytemp\"/>
<cache name=\"users\"
timeToLiveSeconds=\"300\"
maxEntriesLocalHeap=\"1000\"/>
<defaultCache name=\"defaultCache\"
maxElementsInMemory=\"10000\"
eternal=\"false\"
timeToIdleSeconds=\"120\"
timeToLiveSeconds=\"120\"
overflowToDisk=\"false\"
maxElementsOnDisk=\"100000\"
diskPersistent=\"false\"
diskExpiryThreadIntervalSeconds=\"120\"
memoryStoreEvictionPolicy=\"LRU\"/>
ehcache>
server.port=8080
spring.application.name=springboot
# mybatis配置
mybatis.type-aliases-package=com.fu.springboot.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
# druid连接池配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql:///demo
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
@SpringBootConfiguration
@EnableSwagger2
public class SwaggerConfig {
private Logger logger = LoggerFactory.getLogger(SwaggerConfig.class);
@Bean
public Docket docket() {
logger.info(\"Swagger2 ---> docket 执行了...\");
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo()) // 生成接口文档的头部信息
.select() // 表示选择哪些路径和API生成文档,这里是所有
.apis(RequestHandlerSelectors.basePackage(\"com.fu.springboot.controller\")) // 指定接口所在的包
.paths(PathSelectors.any()) // 表示对所有的API进行监控
.build();
}
/**
* 接口文档的头部信息
*/
private ApiInfo apiInfo() {
logger.info(\"Swagger2 ---> apiInfo 执行了...\");
Contact contact = new Contact(\"孔明\", \"暂无url\", \"fyf980921@163.com\");
return new ApiInfoBuilder()
.title(\"SpringBoot整合shiro+Swagger2实现前后端分离\")
.description(\"文档描述\")
.contact(contact)
.version(\"v1.0\")
.build();
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 8142836626401616290L;
private Integer id;
private String name;
private String password;
private String token;
private Date expireDate;
}
public interface ResponseCodeInterface {
/**
* 获取返回码
*/
int getCode();
/**
* 获取返回的消息
*/
String getMsg();
}
public enum BaseResponseCode implements ResponseCodeInterface {
SUCCESS(0, \"操作成功\"),
SYSTEM_ERROR(500001, \"系统错误\"),
METHOD_INVALIDATE(400001, \"数据校验出错\"),
DATA_ERROR(400002, \"传入数据异常\"),
TOKEN_NOT_NULL(401001, \"用户token不存在,请重新登录\"),
TOKEN_ERROR(500002, \"用户身份校验失败,请重新登录\");
private int code;
private String msg;
BaseResponseCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
@Override
public int getCode() {
return this.code;
}
@Override
public String getMsg() {
return this.msg;
}
}
@Data
public class DataResult<T> {
/**
* 码值
*/
private int code = 0;
/**
* 返回的错误信息
*/
private String msg = \"\";
/**
* 返回的数据
*/
private T data;
// 封装构造器
public DataResult(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public DataResult(int code, T data) {
this.code = code;
this.data = data;
}
public DataResult(int code, String msg) {
this.code = code;
this.msg = msg;
}
public DataResult(){
this.code = BaseResponseCode.SUCCESS.getCode();
this.msg = BaseResponseCode.SUCCESS.getMsg();
this.data = null;
}
public DataResult(T data){
this.code=BaseResponseCode.SUCCESS.getCode();
this.msg=BaseResponseCode.SUCCESS.getMsg();
this.data=data;
}
public DataResult(ResponseCodeInterface responseCodeInterface) {
this.code = responseCodeInterface.getCode();
this.msg = responseCodeInterface.getMsg();
}
public DataResult(ResponseCodeInterface responseCodeInterface, T data) {
this.code = responseCodeInterface.getCode();
this.msg = responseCodeInterface.getMsg();
this.data = data;
}
// 不带数据的返回值信息
public static <T>DataResult success() {
return new DataResult();
}
// 带数据的返回值
public static <T>DataResult success(T data) {
return new <T>DataResult(data);
}
// 3个参数的返回值
public static <T>DataResult getResult(int code, String msg, T data) {
return new <T>DataResult(code, msg, data);
}
// 2个参数的返回值(码值,提示信息)
public static <T>DataResult getResult(int code, String msg) {
return new <T>DataResult(code, msg);
}
// 2个参数的返回值(码值,用户信息)
public static <T>DataResult getResult(int code, T data) {
return new <T>DataResult(code, data);
}
/**
* 直接传递一个枚举类型
*/
public static <T>DataResult getResult(BaseResponseCode baseResponseCode) {
return new <T>DataResult(baseResponseCode);
}
public static <T>DataResult getResult(BaseResponseCode baseResponseCode, T data) {
return new <T>DataResult(baseResponseCode, data);
}
}
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 3618625760384608631L;
private int messageCode;
private String defaultMessage;
public BusinessException(int messageCode,String defaultMessage){
super(defaultMessage);
this.messageCode=messageCode;
this.defaultMessage=defaultMessage;
}
public String getDefaultMessage() {
return defaultMessage;
}
public int getMessageCode() {
return messageCode;
}
}
public interface UserMapper {
/**
* 通过用户名查询用户
*/
User getUserByName(String name);
/**
* 查询所有的用户
*/
List<User> getUserAll();
/**
* 更新数据库用户的token
*/
void updateUser(User user);
/**
* 查看token是否存在
*/
User getUserByToken(String token);
}
<mapper namespace=\"com.fu.springboot.mapper.UserMapper\">
<select id=\"getUserByName\" resultType=\"user\">
select * from user where name = #{name}
select>
<select id=\"getUserAll\" resultType=\"user\">
select * from user
select>
<update id=\"updateUser\" parameterType=\"user\">
update user set token = #{token}, expireDate = #{expireDate} where id = #{id}
update>
<select id=\"getUserByToken\" resultType=\"user\">
select * from user where token = #{token}
select>
mapper>
public interface UserService {
/**
* 根据用户名获取用户信息
*/
User getUserByName(String name);
/**
* 登录
*/
User login(User user);
/**
* 更新用户信息
*/
void updateUser(User user);
/**
* 查询所有的用户
*/
List<User> getUserList()throws Exception;
/**
* 判定这个token是否存在
*/
boolean tokenExistsOrNot(String token);
}
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User getUserByName(String name) {
return userMapper.getUserByName(name);
}
/**
* 登录
* 第一步:获取到前端传递过来的用户名
* 第二步:通过用户名 获取用户对象
* 第三步:校验
* 第四步:生成token保存到数据库
* 第五步:将token封装到返回数据里面给前端
*/
@Override
public User login(User user) {
String name = user.getName();
User userResult = this.getUserByName(name);
if (null == userResult) { // 说明用户名不对
throw new BusinessException(400001, \"用户名不正确\");
}
if (!userResult.getPassword().equals(user.getPassword())) {
throw new BusinessException(400002, \"密码错误\");
}
// 生成token,这里用UUID表示
String token = UUID.randomUUID().toString().substring(0, 30);
Date date = new Date();
userResult.setToken(token);
userResult.setExpireDate(date);
// 更新数据库的数据
this.updateUser(userResult);
userResult.setPassword(\"\");
return userResult;
}
@Override
public void updateUser(User user) {
userMapper.updateUser(user);
}
@Override
public List<User> getUserList() throws Exception {
return userMapper.getUserAll();
}
@Override
public boolean tokenExistsOrNot(String token) {
try {
User user = userMapper.getUserByToken(token);
if (null != user) {
return true;
}
} catch (Exception e) {
return false;
}
return false;
}
}
@RestController
@Api(tags = {\"用户接口\"})
@RequestMapping(\"user\")
public class UserController {
@Autowired
private UserService userService;
@PostMapping(\"login\")
@ApiOperation(value = \"用户登录的接口\")
public DataResult<User> login(@RequestBody User user) {
User user1 = userService.login(user);
DataResult<User> result = null;
try {
result = DataResult.success(user1);
} catch (Exception e) {
if (e instanceof BusinessException) {
BusinessException err = (BusinessException) e;
result = DataResult.getResult(err.getMessageCode(), err.getMessage());
} else {
result = DataResult.getResult(BaseResponseCode.SYSTEM_ERROR.getCode(), BaseResponseCode.SYSTEM_ERROR.getMsg());
}
return result;
}
return result;
}
@GetMapping(\"list\")
@ApiOperation(\"获取所有用户的信息\")
@ApiImplicitParam(paramType = \"header\", name = \"token\", value = \"验证身份的token\", required = true, dataType = \"string\")
public Object getUserList() {
DataResult<List<User>> result = null;
try {
List<User> list = userService.getUserList();
result = DataResult.success(list);
} catch (Exception e) {
e.printStackTrace();
result = DataResult.getResult(BaseResponseCode.SYSTEM_ERROR.getCode(), BaseResponseCode.SYSTEM_ERROR.getMsg());
}
return result;
}
}
public class CustomToken extends UsernamePasswordToken {
private static final long serialVersionUID = 561721881796304836L;
/**
* 用户身份唯一的标识
* 这个token是在认证通过之后,用户访问其他资源的时候,来进行身份识别的
*/
private String token;
/**
* 只允许在 CustomToken构造的时候给token赋值
*/
public CustomToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
// 在用户认证通过之后 再访问这个方法 默认返回的是 Realm校验的第一个参数
// Realm校验我们是自己定义的,我们可以自己设置这个方法的返回值
return this.token;
}
}
public class UserRealm extends AuthorizingRealm {
private Logger logger = LoggerFactory.getLogger(UserRealm.class);
@Override
public String getName() {
return \"UserRealm\";
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 取出前端传递过来的token
CustomToken customToken = (CustomToken) authenticationToken;
String token = (String) customToken.getPrincipal();
// 将前端传递过来的token封装到 SimpleAuthenticationInfo 对象中
SimpleAuthenticationInfo simpleAuthorizationInfo = new SimpleAuthenticationInfo(token, token, getName());
logger.info(\"UserRealm ---> 认证身份信息执行...\");
return simpleAuthorizationInfo;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
logger.info(\"UserRealm ---> 授权方法执行...\");
return simpleAuthorizationInfo;
}
}
public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {
@Autowired
private UserService userService;
/**
* @return 返回true代表校验成功,false代表失败
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
// 获取客户端传过来的token
CustomToken customToken = (CustomToken) token;
String tokenClient = (String) customToken.getPrincipal();
// 获取从服务器获取的token(redis,数据库 或者 session)
boolean b = false;
try {
b = userService.tokenExistsOrNot(tokenClient);
} catch (Exception e) {
throw new BusinessException(BaseResponseCode.TOKEN_NOT_NULL.getCode(), BaseResponseCode.TOKEN_NOT_NULL.getMsg());
}
// 判断token是否一致
if (!b) {
throw new BusinessException(BaseResponseCode.TOKEN_ERROR.getCode(), BaseResponseCode.TOKEN_ERROR.getMsg());
}
return true;
}
}
public class CustomAccessControlFilter extends AccessControlFilter {
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 校验身份
try {
// 1. 获取token
String token = request.getHeader(Constant.REQ_TOKEN);
// 2. 判断 token 是否为空
if (StringUtils.isEmpty(token)) { // 用户的身份是非法的
throw new BusinessException(400004, \"用户请求的token不能为空\");
} else { // 用户已经登录,并获取到了token
// 3. 封装token
CustomToken customToken = new CustomToken(token);
// 4. 把token交给shiro做认证,判断身份是否合法
/* 这个方法,用户第一次访问请求(即 登录)的时候,并不会执行
只有在认证成功之后访问其他资源的时候,才会执行
作用是:校验用户身份,而不是登录
*/
getSubject(servletRequest, servletResponse).login(customToken);
return true;
}
} catch (BusinessException e) {
// 如果是这个异常:返回JSON告诉客户端,出现问题了
resultResponse(e.getMessageCode(), e.getDefaultMessage(), servletResponse);
} catch (AuthenticationException e) { // 校验未通过异常
// e.getCause() :该方法返回的是当前异常的实例
if (e.getCause() instanceof BusinessException) { // 表示返回的是自定义的异常
// 将异常实例进行转换
BusinessException err = (BusinessException) e.getCause();
resultResponse(err.getMessageCode(), err.getDefaultMessage(), servletResponse);
} else { // 说明是 shiro 抛出的异常
resultResponse(400001, \"用户身份校验失败\", servletResponse);
}
} catch (AuthorizationException e) { // 授权时出现异常
if (e.getCause() instanceof BusinessException) {
BusinessException err = (BusinessException) e.getCause();
resultResponse(err.getMessageCode(), err.getDefaultMessage(), servletResponse);
} else {
resultResponse(403001, \"用户没有访问权限\", servletResponse);
}
} catch (Exception e) { // 捕获未考虑到的异常,比如系统异常
if (e.getCause() instanceof BusinessException) {
BusinessException err = (BusinessException) e.getCause();
resultResponse(err.getMessageCode(), err.getDefaultMessage(), servletResponse);
} else {
resultResponse(500001, \"服务器开小差了,系统出错\", servletResponse);
}
}
return false;
}
/**
* 这个方法的主要功能就是告诉客户端 一些出错的信息
*/
private void resultResponse(int messageCode, String defaultMessage, ServletResponse servletResponse) {
// 构建返回的数据
JSONObject jsonObject = new JSONObject();
jsonObject.put(\"code\", messageCode);
jsonObject.put(\"msg\", defaultMessage);
// 设置返回的数据类型
/* MediaType.APPLICATION_JSON_UTF8_VALUE ===>>> MediaType.APPLICATION_JSON_VALUE
MediaType.APPLICATION_JSON_UTF8_VALUE 已被标记@Deprecated
自Spring Framework 5.2起不推荐使用,而推荐使用{@link #APPLICATION_JSON_VALUE}
由于主要的浏览器(例如Chrome)现在已符合规范并正确解释了UTF-8特殊字符 不需要{@code charset = UTF-8}参数。
*/
servletResponse.setContentType(MediaType.APPLICATION_JSON.toString());
// 获取输出流
try {
ServletOutputStream out = servletResponse.getOutputStream();
// 将数据写出去
out.write(jsonObject.toJSONString().getBytes());
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@SpringBootConfiguration
public class ShiroConfig {
private Logger logger = LoggerFactory.getLogger(ShiroConfig.class);
// 自定义密码认证器
@Bean
public CustomHashedCredentialsMatcher customHashedCredentialsMatcher() {
CustomHashedCredentialsMatcher hashedCredentialsMatcher = new CustomHashedCredentialsMatcher();
logger.info(\"shiro ---> HashedCredentialsMatcher 执行了...\");
return hashedCredentialsMatcher;
}
// 用户Realm
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(customHashedCredentialsMatcher());
logger.info(\"shiro ---> UserRealm 执行了...\");
return userRealm;
}
// 安全管理器
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(userRealm());
defaultWebSecurityManager.setCacheManager(ehCacheManager());
logger.info(\"shiro ---> SecurityManager 执行了...\");
return defaultWebSecurityManager;
}
// shiro的过滤器
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 配置自定义的校验身份的过滤器
LinkedHashMap<String, Filter> customAccessControlFilter = new LinkedHashMap<>();
customAccessControlFilter.put(\"token\", new CustomAccessControlFilter());
shiroFilterFactoryBean.setFilters(customAccessControlFilter);
// 配置拦截访问路径的过滤器
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put(\"/user/login\", \"anon\");
map.put(\"/swagger/**\",\"anon\");
map.put(\"/v2/api-docs\",\"anon\");
map.put(\"/swagger-ui.html\",\"anon\");
map.put(\"/swagger-resources/**\",\"anon\");
map.put(\"/webjars/**\",\"anon\");
map.put(\"/favicon.ico\",\"anon\");
map.put(\"/captcha.jpg\",\"anon\");
map.put(\"/csrf\",\"anon\");
map.put(\"/**\", \"token,authc\");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
logger.info(\"shiro ---> ShiroFilterFactoryBean 执行了...\");
return shiroFilterFactoryBean;
}
// 缓存管理器
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile(\"classpath:ehcache.xml\");
logger.info(\"shiro ---> EhCacheManager 执行了...\");
return ehCacheManager;
}
/**
* 开启aop的注解的支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
attributeSourceAdvisor.setSecurityManager(securityManager);
return attributeSourceAdvisor;
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
}
@SpringBootApplication
@ComponentScan(\"com.fu.springboot\")
@MapperScan(\"com.fu.springboot.mapper\")
public class AppConfig {
}
访问 http://127.0.0.1:8080/swagger-ui.html ,在页面进行接口测试即可