在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。
如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:
本文以记录学习 shiro 为主,其它内容可能很潦草
我们先了解几个概念,再介绍几种解决方案
什么是有状态?
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。
例如登录:用户登录后,我们把登录者的信息保存在服务端 session 中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。
缺点是什么?
什么是无状态?
微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:
带来的好处是什么呢?
如何实现无状态?
无状态登录的流程:
什么是JWT ?
Json web token,JWT 是目前最流行的跨域认证解决方案。基于json的开放标准(RFC7用于519),以 token的方式代替传统的 session-cookie 模式,可实现无状态、分布式的Web应用授权。用于服务器,客户端传递信息签名验证。
JWT包含三部分数据:
Header
:头部,通常头部有两部分信息:
我们会对头部进行base64编码,得到第一部分数据
Payload
:载荷,就是有效数据,一般包含下面信息:
这部分也会采用base64编码,得到第二部分数据
Signature
:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法生成。用于验证整个数据完整和可靠性
生成的数据格式:token==个人证件 jwt=个人身份证
传统的cookie-session鉴权:
客户端使用用户名和密码登录
服务器端验证账号密码通过后,在 session 里保存一些数据(比如说用户UID,登录时间等等)
服务器向用户返回一个 session_id,写入用户的cookie中
此后用户的每一次请求都用 把 cookie 中的这个 session_id 传给服务器
服务器接收到 session_id 找到之前保存的数据就可以知道用户有没有登录
传统方式的缺点:
JWT 的验证方式
因为 JWT 签发的 token 中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,完全符合了 Rest 的无状态规范。
JWT的优点:
服务器不用 session了,变为无状态。减小了开支
jwt 构成简单,占用很少的字节
json 格式通用。不用语言之间都可以处理
加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:
Spring为我们提供了org.springframework.web.servlet.handler.HandlerInterceptorAdapter
这个适配器,继承此类,可以非常方便的实现自己的拦截器。
他有三个方法:
示意实现 JWT配置类写法:
@Component
public class JwtInterceptor extends HandlerInterceptorAdapter {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 1.通过request获取请求token信息
String authorization = request.getHeader("Authorization");
//判断请求头信息是否为空,或者是否已Bearer开头
if(!StringUtils.isEmpty(authorization) && authorization.startsWith("Bearer")) {
//获取token数据
String token = authorization.replace("Bearer ","");
//解析token获取claims
Claims claims = jwtUtils.parseJwt(token);
if(claims != null) {
//通过claims获取到当前用户的可访问API权限字符串
String apis = (String) claims.get("apis"); //api-user-delete,api-userupdate
//通过handler
HandlerMethod h = (HandlerMethod) handler;
//获取接口上的reqeustmapping注解
RequestMapping annotation =
h.getMethodAnnotation(RequestMapping.class);
//获取当前请求接口中的name属性
String name = annotation.name();
//判断当前用户是否具有响应的请求权限
if(apis.contains(name)) {
request.setAttribute("user_claims",claims);
return true;
}else {
throw new CommonException(ResultCode.UNAUTHORISE);
}
}
}
throw new CommonException(ResultCode.UNAUTHENTICATED);
}
}
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。对于安全控制,我们仅需要引入 spring-boot-starter-security
模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
WebSecurityConfigurerAdapter
:自定义Security策略AuthenticationManagerBuilder
:自定义认证策略@EnableWebSecurity
:开启WebSecurity模式Spring Security 的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
这个概念是通用的,而不是只在Spring Security 中存在。
这里介绍基本的登录登出认证操作,供入门了解
建议通过阅读源码练习,进入 对应的重写方法参数对象查看
参考官网:https://spring.io/projects/spring-security
参考官网:https://spring.io/projects/spring-security
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
@Controller
public class RouterController {
//首页
@RequestMapping({"/","/index"})
public String index(){
return "index";
}
@RequestMapping("/toLogin")
public String tologin(){
return "login";
}
@RequestMapping("/level1/{id}")
public String tologin(@PathVariable("id") int id){
return "views/level1"+id;
}
//...
}
编写 Spring Security 配置类
继承 WebSecurityConfigurerAdapter 类,重写 configure 方法
稍微提一下
该框架有一个很大的特点就是:链式编程
使用 HttpSecurity
对象,编写授权方法
http.formLogin();
开启登录(该框架自动提供了登录页面,也可自己定义)
.loginPage("/toLogin")
,自己定义登录页面http.logout()
,开启自动配置的注销
http.rememberMe()
,开启"记住我"
功能
.rememberMeParameter("remember")
,使用 AuthenticationManagerBuilder
对象 编写认证方法
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//开启自动配置的登录功能:如果没有权限,就会跳转到登录页面!
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin()
.usernameParameter("username")//配置接收登录的用户名和密码的参数!
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login"); // 登陆表单提交请求
//开启自动配置的注销的功能
// /logout 注销请求
// .logoutSuccessUrl("/"); 注销成功来到首页
// sample logout customization,这里也可以选择,清空cookie 与 session
//http.logout().deleteCookies("remove").invalidateHttpSession(false)
http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");
//记住我
//自定义接收前端参数!
http.rememberMe().rememberMeParameter("remember");
}
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
}
}
上面代码示例了从内存中获取认证,下面截取使用数据库方式的官方文档参考:
import javax.sql.DataSource;
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username,password,enabled from users WHERE username=?")
.authoritiesByUsernameQuery("select username,authority from authorities where username=?")
.passwordEncoder(new BCryptPasswordEncoder());
}
记住我功能如何实现的呢?其实非常简单
登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了。如果点击注销,则会删除这个 cookie
我们可以查看浏览器的 cookie
如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭csrf功能;我们试试:在 配置中增加
http.csrf().disable();
http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
命名空间
sec:authorize="isAuthenticated()"
: 是否认证登录!来显示不同的页面
index 首页(部分)
<div class="right menu">
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/tologin}">
<i class="address card icon">i> 登录
a>
div>
<div sec:authorize="isAuthenticated()">
<a class="item">
<i class="address card icon">i>
用户名:<span sec:authentication="principal.username">span>
角色:<span sec:authentication="principal.authorities">span>
a>
div>
<div sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/logout}">
<i class="address card icon">i> 注销
a>
div>
div>
角色认证相关:
根据用户权限,动态显示菜单
<div class="column" sec:authorize="hasRole('vip1')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 1h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon">i> Level-1-1a>div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon">i> Level-1-2a>div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon">i> Level-1-3a>div>
div>
div>
div>
div>
<div class="column" sec:authorize="hasRole('vip2')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 2h5>
<hr>
<div><a th:href="@{/level2/1}"><i class="bullhorn icon">i> Level-2-1a>div>
<div><a th:href="@{/level2/2}"><i class="bullhorn icon">i> Level-2-2a>div>
<div><a th:href="@{/level2/3}"><i class="bullhorn icon">i> Level-2-3a>div>
div>
div>
div>
div>
login 页面 配置提交请求及方式
<form th:action="@{/login}" method="post">
<div class="field">
<label>Usernamelabel>
<div class="ui left icon input">
<input type="text" placeholder="Username" name="username">
<i class="user icon">i>
div>
div>
<div class="field">
<label>Passwordlabel>
<div class="ui left icon input">
<input type="password" name="password">
<i class="lock icon">i>
div>
div>
<input type="submit" class="ui blue submit button"/>
form>
什么是Shiro?
Apache Shiro是一个强大且易用的 Java 安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的 API ,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
与Spring Security的对比
Shiro较之 Spring Security,Shiro在保持强大功能的同时,还在简单性和灵活性方面拥有巨大优势。
Spring Security:
除了不能脱离Spring,shiro的功能它都有。而且 Spring Security对Oauth、OpenID也有支持,Shiro则需要自己手动实现。Spring Security的权限细粒度更高。
Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E5djzB6y-1590212278601)(https://s1.ax1x.com/2020/04/15/JCHHpQ.jpg)]
Authentication
:身份认证/登录,验证用户是不是拥有相应的身份。
Authorization
:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情。
Session Management
:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的。
Cryptography
:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
Web Support
:Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序。
Caching
:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率。
Concurrency
:Apache Shiro 利用它的并发特性来支持多线程应用程序。
Testing
:测试支持的存在来帮助你编写单元测试和集成测试,并确保你的能够如预期的一样安全。
"Run As"
:一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。
"Remember Me"
:记住我。
Subject
:主体,可以看到主体可以是任何可以与应用交互的“用户”;SecurityManager
:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是Shiro的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject 、且负责进行认证和授权、及会话、缓存的管理。Authenticator
:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;Authrizer
:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;Realm
:可以有1个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;SessionManager
:如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所有呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;SessionDAO
:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;CacheManager
:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能Cryptography
:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。也就是说对于我们而言,最简单的一个 Shiro 应用:
以上也可以看出,Shiro不提供维护用户/权限,而是通过 Realm 让开发人员自己注入。
认证流程
首先调用 Subject.login(token)
进行登录,其会自动委托给 Security Manager
,调用之前必须通过
SecurityUtils. setSecurityManager()
设置;
SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
Authenticator 才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现;
Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多Realm身份验证,默认ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多Realm身份验证;
Authenticator 会把相应的 token 传入Realm,从 Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。
授权流程
首先调用 Subject.isPermitted/hasRole
接口,其会委托给 SecurityManager,而 SecurityManager接着会委托给 Authorizer;
Authorizer 是真正的授权者,如果我们调用如 isPermitted(“user:view”)
,其首先会通 PermissionResolver 把字符串转换成相应的 Permission 实例;
在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的角色/权限;
Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个 Realm,会委托给ModularRealmAuthorizer 进行循环判断,如果匹配如 isPermitted/hasRole
会返回true,否则返回false表示授权失败。
用户认证
认证:身份认证/登录,验证用户是不是拥有相应的身份。
基于shiro的认证,是通过 subject 的 login方法完成用户认证工作的。
认证的主要目的,比较用户输入的用户名密码是否和数据库中的一致
用户授权
授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限
授权的主要目的就是查询数据库获取用户的所有角色和权限信息
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.16.16version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redisartifactId>
<version>3.0.0version>
dependency>
dependencies>
server:
port: 8081
spring:
application:
name: ihrm-company #指定服务名
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro_db?useUnicode=true&characterEncoding=utf8
username: root
password: 111111
jpa:
database: MySQL
show-sql: true
open-in-view: true
redis:
host: 127.0.0.1
port: 6379
@Entity
@Table(name = "pe_role")
@Getter
@Setter
public class Role implements Serializable {
private static final long serialVersionUID = 594829320797158219L;
@Id
private String id;
private String name;
private String description;
//角色与用户 多对多
@ManyToMany(mappedBy="roles")
private Set<User> users = new HashSet<User>(0);
//角色与权限 多对多
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name="pe_role_permission",
joinColumns={@JoinColumn(name="role_id",referencedColumnName="id")},
inverseJoinColumns={@JoinColumn(name="permission_id",referencedColumnName="id")})
private Set<Permission> permissions = new HashSet<Permission>(0);
}
/**
* 用户实体类
*/
@Entity
@Table(name = "pe_user")
@Getter
@Setter
/**
* AuthCachePrincipal:
* redis和shiro插件包提供的接口
*/
public class User implements Serializable ,AuthCachePrincipal {
private static final long serialVersionUID = 4297464181093070302L;
/**
* ID
*/
@Id
private String id;
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name="pe_user_role",joinColumns={@JoinColumn(name="user_id",referencedColumnName="id")},
inverseJoinColumns={@JoinColumn(name="role_id",referencedColumnName="id")}
)
private Set<Role> roles = new HashSet<Role>();//用户与角色 多对多
@Override
public String getAuthCacheKey() {
return null;
}
}
@Entity
@Table(name = "pe_permission")
@Getter
@Setter
@NoArgsConstructor
public class Permission implements Serializable {
private static final long serialVersionUID = -4990810027542971546L;
/**
* 主键
*/
@Id
private String id;
private String name;
private String code;
private String description;
}
@SpringBootApplication(scanBasePackages = "cn.cast")
@EntityScan("cn.cast.shiro.domain")
public class ShiroApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroApplication.class, args);
}
//OpenEntityManagerInViewFilter会让session一直到view层调用结束后才关闭
@Bean
public OpenEntityManagerInViewFilter openEntityManagerInViewFilter() {
return new OpenEntityManagerInViewFilter();
}
}
传统登录: 前端发送登录请求 => 接口部分获取用户名密码 => 程序员在接口部分手动控制
shiro:前端发送登录请求 => 接口部分获取用户名密码 => 通过 subject.login => realm域的认证方法
UserController
@RestController
public class UserController {
@Autowired
private UserService userService;
//个人主页
//使用shiro注解鉴权
//@RequiresPermissions() -- 访问此方法必须具备的权限
//@RequiresRoles() -- 访问此方法必须具备的角色
/**
* 1.过滤器:如果权限信息不匹配setUnauthorizedUrl地址
* 2.注解:如果权限信息不匹配,抛出异常
*/
@RequiresPermissions("user-home")
@RequestMapping(value = "/user/home")
public String home() {
return "访问个人主页成功";
}
//添加
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String add() {
return "添加用户成功";
}
//查询
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String find() {
return "查询用户成功";
}
//更新
@RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
public String update(String id) {
return "更新用户成功";
}
//删除
@RequestMapping(value = "/user/{id}",method = RequestMethod.DELETE)
public String delete() {
return "删除用户成功";
}
/**
* 1.传统登录
* 前端发送登录请求 => 接口部分获取用户名密码 => 程序员在接口部分手动控制
* 2.shiro登录
* 前端发送登录请求 => 接口部分获取用户名密码 => 通过subject.login => realm域的认证方法
*
*/
//用户登录
@RequestMapping(value="/login")
public String login(String username,String password) {
//构造登录令牌
try {
/**
* 密码加密:
* shiro提供的md5加密
* Md5Hash:
* 参数一:加密的内容
* 111111 --- abcd
* 参数二:盐(加密的混淆字符串)(用户登录的用户名)
* 111111+混淆字符串
* 参数三:加密次数
*
*/
password = new Md5Hash(password,username,3).toString();
UsernamePasswordToken upToken = new UsernamePasswordToken(username,password);
//1.获取subject
Subject subject = SecurityUtils.getSubject();
//获取session
String sid = (String) subject.getSession().getId();
//2.调用subject进行登录
subject.login(upToken);
return "登录成功";
}catch (Exception e) {
return "用户名或密码错误";
}
}
}
UserDao
/**
* 用户数据访问接口
*/
public interface UserDao extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
//根据手机号获取用户信息
User findByUsername(String name);
}
UserService
@Service
public class UserService {
@Autowired
private UserDao userDao;
public User findByName(String name) {
return this.userDao.findByUsername(name);
}
public List<User> findAll() {
return userDao.findAll();
}
}
BaseExceptionHandler
/**
* 自定义的公共异常处理器
* 1.声明异常处理器
* 2.对异常统一处理
*/
@ControllerAdvice
public class BaseExceptionHandler {
@ExceptionHandler(value = AuthorizationException.class)
@ResponseBody
public String error(HttpServletRequest request, HttpServletResponse response,AuthorizationException e) {
return "未授权";
}
}
自定义 Realm
Realm域:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源
CustomRealm
/**
* 自定义的realm
*/
public class CustomRealm extends AuthorizingRealm {
public void setName(String name) {
super.setName("customRealm");
}
@Autowired
private UserService userService;
/**
* 授权方法
* 操作的时候,判断用户是否具有响应的权限
* 先认证 -- 安全数据
* 再授权 -- 根据安全数据获取用户具有的所有操作权限
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//1.获取已认证的用户数据
User user = (User) principalCollection.getPrimaryPrincipal();//得到唯一的安全数据
//2.根据用户数据获取用户的权限信息(所有角色,所有权限)
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> roles = new HashSet<>();//所有角色
Set<String> perms = new HashSet<>();//所有权限
for (Role role : user.getRoles()) {
roles.add(role.getName());
for (Permission perm : role.getPermissions()) {
perms.add(perm.getCode());
}
}
info.setStringPermissions(perms);
info.setRoles(roles);
return info;
}
/**
* 认证方法
* 参数:传递的用户名密码
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1.获取登录的用户名密码(token)
UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
String username = upToken.getUsername();
String password = new String( upToken.getPassword());
//2.根据用户名查询数据库
User user = userService.findByName(username);
//3.判断用户是否存在或者密码是否一致
if(user != null && user.getPassword().equals(password)) {
//4.如果一致返回安全数据
//构造方法:安全数据,密码,realm域名
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
return info;
}
//5.不一致,返回null(抛出异常)
return null;
}
public static void main(String[] args) {
System.out.println(new Md5Hash("123456","wangwu",3).toString());
}
}
在 shiro 里所有的用户的会话信息都会由 Shiro 来进行控制,shiro 提供的会话可以用于 JavaSE/JavaEE 环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。通过 Shiro 的会话管理器(SessionManager)进行统一的会话管理
什么是 shiro 的会话管理?
SessionManager(会话管理器):管理所有 Subject 的 session 包括创建、维护、删除、失效、验证等工作。SessionManager是顶层组件,由SecurityManager管理
shiro提供了三个默认实现:
Shiro结合redis的统一会话管理
自定义的 sessionManager
/**
* 自定义的sessionManager
*/
public class CustomSessionManager extends DefaultWebSessionManager {
/**
* 头信息中具有sessionid
* 请求头:Authorization: sessionid
*
* 指定sessionId的获取方式
*/
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//获取请求头Authorization中的数据
String id = WebUtils.toHttp(request).getHeader("Authorization");
if(StringUtils.isEmpty(id)) {
//如果没有携带,生成新的sessionId
return super.getSessionId(request,response);
}else{
//返回sessionId;
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
}
}
在配置中还需配置,实现思路,代码在下节
SecurityManager
是 Shiro 架构的心脏,用于协调内部的多个组件完成全部认证授权的过程。例如通过调用 realm 完成认证与登录。使用基于 springboot的配置方式完成 SecurityManager,Realm 的装配
ShiroConfiguration
@Configuration
public class ShiroConfiguration {
//1.创建realm
@Bean
public CustomRealm getRealm() {
return new CustomRealm();
}
//2.创建安全管理器
@Bean
public SecurityManager getSecurityManager(CustomRealm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
//将自定义的会话管理器注册到安全管理器中
securityManager.setSessionManager(sessionManager());
//将自定义的redis缓存管理器注册到安全管理器中
securityManager.setCacheManager(cacheManager());
return securityManager;
}
//3.配置shiro的过滤器工厂
/**
* 再web程序中,shiro进行权限控制全部是通过一组过滤器集合进行控制
*
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
//1.创建过滤器工厂
ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
//2.设置安全管理器
filterFactory.setSecurityManager(securityManager);
//3.通用配置(跳转登录页面,为授权跳转的页面)
filterFactory.setLoginUrl("/autherror?code=1");//跳转url地址
filterFactory.setUnauthorizedUrl("/autherror?code=2");//未授权的url
//4.设置过滤器集合
/**
* 设置所有的过滤器:有顺序map
* key = 拦截的url地址
* value = 过滤器类型
*
*/
Map<String,String> filterMap = new LinkedHashMap<>();
//filterMap.put("/user/home","anon");//当前请求地址可以匿名访问
//具有某中权限才能访问
//使用过滤器的形式配置请求地址的依赖权限
//filterMap.put("/user/home","perms[user-home]"); //不具备指定的权限,跳转到setUnauthorizedUrl地址
//使用过滤器的形式配置请求地址的依赖角色
//filterMap.put("/user/home","roles[系统管理员]");
filterMap.put("/user/**","authc");//当前请求地址必须认证之后可以访问
filterFactory.setFilterChainDefinitionMap(filterMap);
return filterFactory;
}
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
/**
* 1.redis的控制器,操作redis
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
return redisManager;
}
/**
* 2.sessionDao
*/
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO sessionDAO = new RedisSessionDAO();
sessionDAO.setRedisManager(redisManager());
return sessionDAO;
}
/**
* 3.会话管理器
*/
public DefaultWebSessionManager sessionManager() {
CustomSessionManager sessionManager = new CustomSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* 4.缓存管理器
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
//开启对shior注解的支持
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
shiroFilterFactoryBean.setLoginUrl();
设置的 urlshiro 支持基于过滤器的授权方式也支持注解的授权方式
基于配置的授权:
//使用过滤器的形式配置请求地址的依赖角色
//filterMap.put("/user/home","roles[系统管理员]");
filterMap.put("/user/**","authc");//当前请求地址必须认证之后可以访问
基于注解的授权:
//查询
@RequiresPermissions(value = "user-find")
public String find() {
return "查询用户成功";
}
//查询
@RequiresRoles(value = "系统管理员")
public String find() {
return "查询用户成功";
}
Zuul 作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的,这样我们就能实现 限流,灰度发布,权限控制 等等。
接下来,我们在Zuul编写拦截器,对用户的token进行校验,如果发现未登录,则进行拦截。
关注一下末尾的 filter ,并不是所有的路径我们都需要拦截,所以,我们需要在拦截时,配置一个白名单,如果在名单内,则不进行拦截。
server:
port: 10010
spring:
application:
name: leyou-gateway
eureka:
client:
registry-fetch-interval-seconds: 5
service-url:
defaultZone: http://127.0.0.1:10086/eureka
zuul:
prefix: /api # 路由路径前缀
routes:
item-service: /item/** # 商品微服务的映射路径
search-service: /search/** #路由到搜索微服务
user-service: /user/** # 用户微服务
auth-service: /auth/** # 授权中心微服务
cart-service: /cart/** # 购物车微服务
order-service: /order/** # 购物车微服务
add-host-header: true
sensitive-headers: # 覆盖默认敏感头信息
leyou:
jwt:
pubKeyPath: C:\\tmp\\rsa\\rsa.pub # 公钥地址
cookieName: LY_TOKEN # token
filter:
allowPaths:
- /api/auth
- /api/search
- /api/user/register
- /api/user/check
- /api/user/code
- /api/item
@Component
@ConfigurationProperties(prefix = "leyou.filter")
public class FilterProperties {
private List<String> allowPaths;
public List<String> getAllowPaths() {
return allowPaths;
}
public void setAllowPaths(List<String> allowPaths) {
this.allowPaths = allowPaths;
}
}
@Component
@ConfigurationProperties(prefix = "leyou.jwt")
public class JwtProperties {
private String pubKeyPath;// 公钥
private PublicKey publicKey; // 公钥
private String cookieName; //cookie
private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);
/**
* @PostContruct:在构造方法执行之后执行该方法
*/
@PostConstruct
public void init(){
try {
// 获取公钥和私钥
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
} catch (Exception e) {
logger.error("初始化公钥和私钥失败!", e);
throw new RuntimeException();
}
}
//省略 getter与setter
}
重头:配置过滤器
继承 ZuulFilter,重写里面方法
shouldFilter
:返回一个Boolean
值,判断该过滤器是否需要执行。返回true执行,返回false不执行。run
:过滤器的具体业务逻辑。filterType
:返回字符串,代表过滤器的类型。包含以下4种:
pre
:请求在被路由之前执行route
:在路由请求时调用post
:在route和errror过滤器之后调用error
:处理请求时发生错误调用filterOrder
:通过返回的 int 值来定义过滤器的执行顺序,数字越小优先级越高。@Component
@EnableConfigurationProperties({JwtProperties.class, FilterProperties.class})
public class LoginFilter extends ZuulFilter {
@Autowired
private JwtProperties jwtProperties;
@Autowired
private FilterProperties filterProperties;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 10;
}
@Override
public boolean shouldFilter() {
//获取context
RequestContext context = RequestContext.getCurrentContext();
//获取request对象
HttpServletRequest request = context.getRequest();
//获取请求路径
String url = request.getRequestURL().toString();
//获取白名单
List<String> allowPaths = this.filterProperties.getAllowPaths();
// 判断白名单
// 遍历允许访问的路径
for (String allowPath : allowPaths) {
if (StringUtils.contains(url,allowPath)){
return false;
}
}
return true;
}
@Override
public Object run() throws ZuulException {
//获取context
RequestContext context = RequestContext.getCurrentContext();
//获取request对象
HttpServletRequest request = context.getRequest();
//获取token
String token = CookieUtils.getCookieValue(request, this.jwtProperties.getCookieName());
/*if (StringUtils.isBlank(token)){
// 校验出现异常,返回403
context.setSendZuulResponse(false);
context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}*/
try {
//解析
JwtUtils.getInfoFromToken(token,this.jwtProperties.getPublicKey());
} catch (Exception e) {
e.printStackTrace();
// 校验出现异常,返回403
context.setSendZuulResponse(false);
context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
}
HttpServletRequest request = context.getRequest();
//获取请求路径
String url = request.getRequestURL().toString();
//获取白名单
List<String> allowPaths = this.filterProperties.getAllowPaths();
// 判断白名单
// 遍历允许访问的路径
for (String allowPath : allowPaths) {
if (StringUtils.contains(url,allowPath)){
return false;
}
}
return true;
}
@Override
public Object run() throws ZuulException {
//获取context
RequestContext context = RequestContext.getCurrentContext();
//获取request对象
HttpServletRequest request = context.getRequest();
//获取token
String token = CookieUtils.getCookieValue(request, this.jwtProperties.getCookieName());
/*if (StringUtils.isBlank(token)){
// 校验出现异常,返回403
context.setSendZuulResponse(false);
context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}*/
try {
//解析
JwtUtils.getInfoFromToken(token,this.jwtProperties.getPublicKey());
} catch (Exception e) {
e.printStackTrace();
// 校验出现异常,返回403
context.setSendZuulResponse(false);
context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
}