技术标签: spring boot java shiro 后端java
本文章参考两位大佬写的博客完成的
学习shiro之前,建议先学习springsecurity,将两个框架进行对比的方式学习,会有更加深的印象。我之前写过一篇关于springsecurity的博客,你可以参考参考,个人感觉挺详细的。超全的springboot+springsecurity前后端分离简单实现!!!_码上编程的博客-程序员秘密。
本篇文章是使用shiro做认证授权的,基于token的前后端分离的小demo,而token我把它放在redis缓存里面,极大程度地模拟实战效果,本篇文章每一步都有详细步骤,看不懂可以看多几遍!!! 源代码在此文章最底部!!!
未登录情况下访问目标资源, 将会提示需要登录的字样。 /hello和/index是我在controller实现的简单接口。
账号错误,并返回登录失败的字样
密码错误,并返回登录失败的字样
登录并返回登录成功字样以及token
登录成功的情况下访问拥有指定权限的目标资源,并返回该目标资源
登录成功的情况下访问不具有指定权限的目标资源,并返回权限不足的提示字样
注销,并返回注销成功字样,同时删除缓存里面的token值
springboot、shiro、mybatis-plus、mysql、redis、gson、lombok
简单理解: shiro最重要的三个部分:
复杂理解就在流程图里面: 强烈建议搞清楚流程图!!!!!
- 不走doGetAuthenticationInfo(AuthenticationToken token) 方法, 通过在pom文件添加这样的依赖即可解决此问题! <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
- 拦截所有路径位置要放正确,要不然它会拦截所有路径,即 filterMap.put("/**","auth"); 要放在最后面
filterMap.put("/login","anon"); //放行login接口
filterMap.put("/logout","anon"); //放行logout接口
filterMap.put("/**","auth"); //拦截所有路径
pom文件
<dependencies>
<!--解决doGetAuthorizationInfo不生效问题-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 配置使用redis启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--gson可以将对象转成json字符串-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--集成jwt实现token认证-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!--模板引擎-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.2</version>
</dependency>
<!--自动生成代码依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
<scope>test</scope>
</dependency>
<!--整合Shiro安全框架-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
数据库设计 ,这里我偷懒了,用的还是springsecurity那个例子的数据库
User.java
Data
@EqualsAndHashCode(callSuper = false)
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String account;
private String password;
private String role;
}
UserMapper.java继承BaseMapper<User>,BaseMapper<User>是mybatis-plus封装好大量基本sql的一个类,直接调用指定代码即可,不用手写sql,加快开发速度。
@Repository
public interface UserMapper extends BaseMapper<User> {
}
UserService.java
public interface UserService extends IService<User> {
//根据账号查找用户
User findByUsername(String username);
}
UserServiceImpl.java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
UserMapper userMapper;
@Override
public User findByUsername(String username) {
//相当于select * from user where account='${username}'
QueryWrapper<User> wrapper=new QueryWrapper<>();
wrapper.eq("account",username);
//user即为查询结果
return userMapper.selectOne(wrapper);
}
}
Msg.java,封装了返回的结果集,这个看个人的,怎么开心怎么来!
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Msg {
int code; //错误码
String Message; //消息提示
Map<String,Object> data=new HashMap<String,Object>(); //数据
//无权访问
public static Msg denyAccess(String message){
Msg result=new Msg();
result.setCode(300);
result.setMessage(message);
return result;
}
//操作成功
public static Msg success(String message){
Msg result=new Msg();
result.setCode(200);
result.setMessage(message);
return result;
}
//客户端操作失败
public static Msg fail(String message){
Msg result=new Msg();
result.setCode(400);
result.setMessage(message);
return result;
}
public Msg add(String key,Object value){
this.data.put(key,value);
return this;
}
}
ShiroConfig.java,详细请看注释
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
//关联 DefaultWebSecurityManager
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加shiro内置的过滤器
/*
* anon: 无需认证就可以访问
* authc: 必须认证才能访问
* user: 必须拥有记住我功能才能用
* perms: 拥有对某个资源的权限才能访问
* role: 拥有某个角色权限才能访问
* */
//添加过滤器
Map<String,Filter> filters=new HashMap<>();
filters.put("auth",new AuthFilter()); //自定义的认证授权过滤器
shiroFilterFactoryBean.setFilters(filters); //添加自定义的认证授权过滤器
//要拦截的路径放在map里面
Map<String,String> filterMap=new LinkedHashMap<String,String>();
filterMap.put("/login","anon"); //放行login接口
filterMap.put("/logout","anon"); //放行logout接口
filterMap.put("/**","auth"); //拦截所有路径, 它自动会跑到 AuthFilter这个自定义的过滤器里面
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
//配置securityManager的实现类,变向的配置了securityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(AuthRealm authRealm){
DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager();
//关联realm
defaultWebSecurityManager.setRealm(authRealm);
/*
* 关闭shiro自带的session
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
return defaultWebSecurityManager;
}
//将自定义realm注入到 DefaultWebSecurityManager
@Bean
public AuthRealm authRealm(){
return new AuthRealm();
}
//通过调用Initializable.init()和Destroyable.destroy()方法,从而去管理shiro bean生命周期
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
//开启shiro权限注解
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(defaultWebSecurityManager);
return advisor;
}
}
自定义认证授权过滤器 AuthFilter , 正常来说对于常用部分需要封装,这样代码冗余度就不会那么大,也比较优雅,但是这只是个小demo,不想把它弄的那么复杂、不利于理解,索性就不封装了。
1、用户发起请求首先进入isAccessAllowed()方法,拦截除了option请求 以外的请求, 浏览器机制就是:在发post、get请求之前,首先会发个option请求进行试探,所以要放行option请求通过,否则你的get、post请求永远进不来。
2、token不为空的情况下则生成属于自己的token,即创建一个类使其继承 UsernamePasswordToken此类
3、然后进入onAccessDenied()方法去校验token是否存在,并执行executeLogin(request,response)方法
public class AuthFilter extends AuthenticatingFilter {
Gson gson=new Gson();
//生成自定义token
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest= (HttpServletRequest) request;
//从header中获取token
String token=httpServletRequest.getHeader("token");
return new AuthToken(token);
}
//所有请求全部拒绝访问
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//允许option请求通过
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
return false;
}
//拒绝访问的请求,onAccessDenied方法先获取 token,再调用executeLogin方法
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest= (HttpServletRequest) request;
HttpServletResponse httpServletResponse= (HttpServletResponse) response;
String token=httpServletRequest.getHeader("token"); //获取请求token
//StringUtils.isBlank(String str) 判断str字符串是否为空或者长度是否为0
if(org.apache.commons.lang3.StringUtils.isBlank(token)){
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Allow-Origin",httpServletRequest.getHeader("Origin") );
httpServletResponse.setCharacterEncoding("UTF-8");
Msg msg= Msg.fail("请先登录");
httpServletResponse.getWriter().write(gson.toJson(msg));
return false;
}
return executeLogin(request,response);
}
//token失效时调用
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest= (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpResponse.setCharacterEncoding("UTF-8");
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
Msg msg=Msg.fail("登录凭证已失效,请重新登录");
httpResponse.getWriter().write(gson.toJson(msg));
} catch (IOException e1) {
}
return false;
}
}
自定义 AuthToken.java 去继承 UsernamePasswordToken这个类,因为源码里的subject.login(token)需要传入一个自定义的token参数
public class AuthToken extends UsernamePasswordToken{
String token;
public AuthToken(String token){
this.token=token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
AuthRealm.java是自定义的realm,继承AuthorizingRealm,实现doGetAuthenticationInfo(AuthenticationToken token)认证和 doGetAuthorizationInfo(PrincipalCollection principals)授权
public class AuthRealm extends AuthorizingRealm {
@Autowired
UserServiceImpl userServiceImpl;
@Autowired
StringRedisTemplate stringRedisTemplate;
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取前端传来的token
String accessToken= (String) token.getPrincipal();
//redis缓存中这样存值, key为token,value为username
//根据token去缓存里查找用户名
String username=stringRedisTemplate.opsForValue().get(accessToken);
if(username==null){
//查找的用户名为空,即为token失效
throw new IncorrectCredentialsException("token失效,请重新登录");
}
User user = userServiceImpl.findByUsername(username);
if(user==null){
throw new UnknownAccountException("用户不存在!");
}
//此方法需要返回一个AuthenticationInfo类型的数据
// 因此返回一个它的实现类SimpleAuthenticationInfo,将user以及获取到的token传入它可以实现自动认证
SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo(user,accessToken,"");
return simpleAuthenticationInfo;
}
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//从认证那里获取到用户对象User
User user = (User) principals.getPrimaryPrincipal();
//此方法需要一个AuthorizationInfo类型的返回值,因此返回一个它的实现类SimpleAuthorizationInfo
//通过SimpleAuthorizationInfo里的addStringPermission()设置用户的权限
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addStringPermission(user.getRole());
return simpleAuthorizationInfo;
}
自定义异常处理器 MyExceptionHandler.java
//@ControllerAdvice可以实现全局异常处理,可以简单理解为增强了的controller
@ControllerAdvice
public class MyExceptionHandler {
//捕获AuthorizationException的异常
@ExceptionHandler(value = AuthorizationException.class)
@ResponseBody
public Msg handleException(AuthorizationException e) {
Msg msg=Msg.denyAccess("权限不足呀!!!!!");
return msg;
}
}
UserController.java
@RestController
public class UserController {
@Autowired
UserServiceImpl userServiceImpl;
//通过java去操作redis缓存string类型的数据
@Autowired
StringRedisTemplate stringRedisTemplate;
//需要权限为ROLE_USER才能访问/index
@RequiresPermissions("ROLE_USER")
@GetMapping("/index")
public Msg index(@RequestHeader String token){
return Msg.success("index");
}
//需要权限ROLE_ADMIN才能访问hello
@RequiresPermissions("ROLE_ADMIN")
@GetMapping("/hello")
public Msg hello(@RequestHeader String token){
return Msg.success("hello");
}
//登录接口
@PostMapping("/login")
public Msg login(@RequestParam("username")String username,@RequestParam("password")String password){
User user = userServiceImpl.findByUsername(username);
Msg msg=null;
if (user == null) {
msg = Msg.fail("账号错误");
} else if (!password.equals(user.getPassword())) {
msg = Msg.fail("密码错误");
} else {
//通过UUID生成token字符串,并将其以string类型的数据保存在redis缓存中,key为token,value为username
String token= UUID.randomUUID().toString().replaceAll("-","");
stringRedisTemplate.opsForValue().set(token,username,3600,TimeUnit.SECONDS);
msg=Msg.success("登录成功").add("token",token);
}
return msg;
}
//注销接口
@PostMapping("/logout")
public Msg logout(@RequestHeader("token")String token){
//删除redis缓存中的token
stringRedisTemplate.delete(token);
return Msg.success("注销成功");
}
}
application.yml配置文件
server:
port: 80
spring:
datasource:
url: jdbc:mysql://localhost:3306/springsecurity_test?characterEncoding=utf8&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
源代码在https://gitee.com/liu-wenxin/shiro_token_demo.git,通过git clone https://gitee.com/liu-wenxin/shiro_token_demo.git 下载使用
以前在cmd终端执行XX.py文件可以直接用python XX.py ,如下图:但是如果写的文件里面有函数,我要导入这个函数就不好弄了,下面是我执行文件函数的步骤:1.按windows+R2.输入cmd,打开终端3,找到要执行文件的地址,(我的文件在)所以我就一步步转到kNN.py的目录下4.这个时候也就是最重要的一步,在knn.py目录下输入python,...
一、SQL Profiler工具简介SQL Profiler是一个图形界面和一组系统存储过程,其作用如下:图形化监视SQL Server查询;在后台收集查询信息;分析性能;诊断像死锁之类的问题;调试T-SQL语句;模拟重放SQL Server活动;也可以使用SQL Profiler捕捉在SQL Server实例上执行的活动。这样的活动被称为Profiler跟踪...
JAVA程序员面试32问 第一,谈谈final, finally, finalize的区别。final-修饰符(关键字)如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在声明时给定初值,而在以后的引
实验九 TCP 协议分析实验1.TCP 协议介绍**TCP 是传输控制协议 (Transmission Control Protocal)的缩写,提供面向连接的可靠的传输服务。在TCP/IP 体系中,HTTP、FTP、SMTP 等协议都是使用TCP 传输方式的。(1)TCP 报文格式图1 TCP 报文段格式TCP 报文分为首部和数据两个部分。如图1 所示,TCP 报文段首部的前20 字节是固定的,后面有4 ×n 字节是可选项。其中:源端口和目的端口:各2 字节,用于区分源端和目的端的多个应
不知啥时候,突然开机就显示这个错误,强迫症表示很难受啊!一开始卸载BJCA之后发现还不行,最后发现是自己没有卸载干净,哭晕o(╥﹏╥)o记住一定要把BJCA相关的全部卸载,最后还有ePass3000卸载掉!!!之后重新安装BJCA程序即可。...
ros 中的navigation可以根据机器人自动实现路径规划,自主定位。其中的amcl实现了一种基于蒙特卡洛的粒子滤波定位方法。本文测试环境为ubuntu18.04 + ros melodic。1. ros安装详见官方网站安装步骤:http://wiki.ros.org/melodic/Installation/Ubuntu2. navigation 安装...
特别是认证考试报名的信息更新如下:考试资格获取的规定参加SAP全球顾问资格认证考试,您需要具备如下条件并获得SAP官方培训部审核通过后,方可报名参加:1. 参加SAP官方提供的培训课程达到指定的天数,课程可以包括标准课程,顾问学院课程2. 参加SAP官方授权合作伙伴,在授权课程范围内提供的培训课程达到指定的天数3. 购买并学习了SAP Learning Hu
CMD进入hyper-v提示界面:sconfigget-windowsfeature,用来查看 windows组件Add-windowsfeature功能对应命令,用来安装所需功能。get-command –module mpio获取MPIO配置命令mpiocpl 多路径管理对话磁盘管理:diskpart 磁盘管理lisk disk 查看磁盘set disk 1 选定磁盘a...
一、Keraskeras 是一个高层神经网络API,Keras由纯python编写而成并基于Tensorflow、Theano以及CNTK后端。可以通过import keras查看keras用的什么后端,例如输出的是,可以在keras.json文件修改后端或者在代码中临时修改import osos.environ[‘KERAS_BACKEND’]=‘tensorflow’。二、K...
"CSV" 是逗号分隔文件 (Comma Separated Values)的首字母英文缩写1. 是一种用来存储数据的纯文本格式,通常用于电子表格或数据库软件。2. 在 CSV 文件中,数据"栏"以逗号分隔,可允许程序通过读取文件为数据重新&lt;wbr&gt;&lt;/wbr&gt;创建正确的栏结构,并在每次遇到逗号时开始新的一栏。一般格式第一行写列名(不同栏目使用逗号分割);第二行开始往...
基于物理的材质可以产生更准确并且通常更加自然的外观,在所有照明环境中都可以同样完美地工作!官方说明转载于:https://www.cnblogs.com/timy/p/10049945.html...