赞
踩
本文仅为个人学习笔记,通过简单的框架搭建来初步学习Spring Security及OAuth2,文中部分方法注解已过时,但仅为学习使用
认证:认证用户的合法性,如登录
授权:登录后,用户具有什么操作权限
会话:将用户认证信息保存起来,避免重复认证(常见方式session,token等)
整体的思想:登录认证获得一个权限标识与权限相关信息,存入会话中(session,结合redis等),使得在有效期内无需重复登录或者请求接口校验用户的权限
RBAC:Role-Based Access Control 基于角色的访问控制,由用户,角色,权限组成
简单来说就是一个用户,具有什么角色,每个角色具有什么权限,从而决定改用户具有什么权限
pom增加依赖
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-thymeleaf</artifactId>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
yml配置文件
- server:
- port: 5555
-
- spring:
- application:
- name: security-demo
- thymeleaf:
- prefix: classpath:templates/
- suffix: .html
实体
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- public class User {
- private String userId;
- private String userName;
- private String password;
- private List<Role> roles = new ArrayList();
- }
-
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- public class Role {
- private String roleId;
- private String roleName;
- private List<Resource> resources = new ArrayList();
- }
-
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- public class Resource {
- private String resourceId;
- private String resourceName;
- private String resourceType;
- }
controller
- @Controller
- @RequestMapping("/auth")
- public class LoginController {
-
- @Resource
- private AuthService authService;
-
- @GetMapping
- public String test(){
- return "success";
- }
- //首页
- @RequestMapping("/index")
- public String index(){
- return "index";
- }
- //登录
- @RequestMapping("/login")
- public String login(String userName,String password, HttpServletRequest request){
- User query = new User();
- query.setUserName(userName);
- query.setPassword(password);
- User userResult = this.authService.getUser(query);
- if(null == userResult){
- return "failed";
- }
- request.getSession().setAttribute("current_user",userResult);
- return "success";
- }
- //获取当前用户
- @PostMapping("/getCurrUser")
- public User getCurrentUser(HttpSession session){
- return (User)session.getAttribute("current_user");
- }
- //登出
- @PostMapping("logout")
- public ResponseEntity logout(HttpSession session){
- session.removeAttribute("current_user");
- return ResponseEntity.ok("已退出登录");
- }
- }
service
- @Service
- public class AuthService {
- @Resource
- private AuthMapper authMapper;
- //查询用户
- public User getUser(User user){
- User userResult = this.authMapper.getUser(user);
- return userResult;
- }
- }
mapper模拟数据库
- @Component
- public class AuthMapper {
- //模拟数据库
- List<User> userDataBase = Arrays.asList(new User("1","admin","123456",null,null));
-
- public User getUser(User user){
- List<User> list = userDataBase.stream().filter(e -> e.getUserName().equals(user.getUserName()) && e.getPassword().equals(user.getPassword()))
- .collect(Collectors.toList());
- return list.size() > 0 ? list.get(0) : null;
- }
- }
简单界面
resources/templates新增界面
登录界面
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>登录页面</title>
- </head>
- <body>
- <form action="/auth/login" method="post">
- 用户名:<input type="text" name="userName" required><br/>
- 密 码:<input type="text" name="password" required><br/>
- <input type="submit" value="登录"> <input type="reset" value="重置">
- </form>
- </body>
- </html>
登录成功界面
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>登录成功</title>
- </head>
- <body>
- 登录成功
- </body>
- </html>
登录失败界面
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>失败</title>
- </head>
- <body>
- 账号或者密码失败
- </body>
- </html>
到此可以启动项目,输入账号密码,界面正常跳转,到此步认证完成
修改登录成功后的界面
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>登录成功</title>
- </head>
- <body onload="lordData();">
- 登录成功
- <div>当前用户:</div>
- <div id="currentUser"></div>
- <br/>
- <div>获取苹果:</div>
- <div id="getApple"></div>
-
- <div>获取香蕉:</div>
- <div id="getBanana"></div>
-
- <br/>
- <div onclick="logout();">退出登录</div>
- <br/>
-
- <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
- <script type="text/javascript">
-
- function lordData(){
- currentUser();
- getApple();
- getBanana();
- }
-
- function logout(){
- $.ajax({
- //请求方式
- type : "POST",
- //请求的媒体类型
- // contentType: "application/json;charset=UTF-8",
- //请求地址
- url : "/auth/logout",
- //数据,json字符串
- data : {},
- //请求成功
- success : function(result) {
- //result = JSON.stringify(result);
- alert(result)
- },
- //请求失败,包含具体的错误信息
- error : function(error){
- alert(error)
- }
- });
- }
-
- function currentUser() {
- $.ajax({
- type : "POST",
- url : "/auth/getCurrUser",
- data : {},
- success : function (result) {
- result = JSON.stringify(result);
- $('#currentUser').html("success:" + result);
- },
- error : function (error) {
- $('#currentUser').html("error:" + error);
- }
- })
- }
-
- function getApple() {
- $.ajax({
- type : "GET",
- url : "/apple",
- data : {},
- success : function (result) {
- $('#getApple').html("success:" + result);
- },
- error : function (error) {
- $('#getApple').html("error:" + error);
- }
- })
- }
-
- function getBanana() {
- $.ajax({
- type : "GET",
- url : "/banana",
- data : {},
- success : function (result) {
- //result = JSON.stringify(result);
- $('#getBanana').html("success:" + result);
- },
- error : function (error) {
- $('#getBanana').html("error:" + error);
- }
- })
- }
- </script>
- </body>
- </html>
新增两个资源获取接口
- @RestController
- @RequestMapping("/apple")
- public class AppleController {
- @GetMapping
- public String getApple(){
- return "资源1苹果";
- }
- }
-
- @RestController
- @RequestMapping("/banana")
- public class BnanaController {
- @GetMapping
- public String getBanana(){
- return "资源2香蕉";
- }
- }
启动项目,登录后,界面会去请求对应的接口获得数据
但此时接口均为做权限的限制,故通过定义适配器+拦截器进行权限的拦截
完善模拟数据库的权限数据
登录成功后,将用户及其权限信息存入session
创建苹果,香蕉资源,再创建管理员角色(苹果,香蕉资源)和苹果经销商角色(苹果资源),
再创建管理员用户(管理员角色(苹果,香蕉资源)),只卖苹果商家用户(苹果经销商角色(苹果资源))
- @Component
- public class AuthMapper {
-
- public User getUser(User user){
- List<User> list = this.getUserList().stream().filter(e -> e.getUserName().equals(user.getUserName()) && e.getPassword().equals(user.getPassword()))
- .collect(Collectors.toList());
- return list.size() > 0 ? list.get(0) : null;
- }
-
- //模拟用户数据库
- public List<User> getUserList(){
-
- List<User> users = new ArrayList<>();
-
- //两个资源呢数据
- Resource appleResource = new Resource("1","apple","1");
- Resource bananaResource = new Resource("2","banana","1");
-
- //创建三种角色 管理员 苹果卖家
- //管理员角色具有苹果 香蕉数据查看权限
- Role adminRole = new Role("1","admin",Arrays.asList(appleResource,bananaResource));
- //苹果卖家只能看到苹果数据的权限
- Role appleRole = new Role("2","appleRole",Arrays.asList(appleResource));
-
-
- //创建两个用户 超级管理员具有admin角色
- User admin = new User("1","admin","123456",Arrays.asList(adminRole));
- users.add(admin);
- //苹果用户 具有 appleRole角色
- User appleOnly = new User("1","apple","123456",Arrays.asList(adminRole));
- users.add(appleOnly);
- return users;
- }
- }
创建应用上下文配置MyWebAppConfigurer
(在此处可配置接口的拦截器)
- @Component
- public class MyWebAppConfigurer implements WebMvcConfigurer {
-
- @Resource
- private AuthInterceptor authInterceptor;
-
- //启动界面的简单配置
- @Override
- public void addViewControllers(ViewControllerRegistry registry) {
- //重定向至主页
- registry.addViewController("/").setViewName("redirect:/index.html");
- }
-
- //配置权限拦截器
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(authInterceptor).addPathPatterns("/**");
- }
- }
创建拦截器
对登录状态及资源的范问权限进行拦截
- @Component
- public class AuthInterceptor extends HandlerInterceptorAdapter {
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- //设置不需要登录即刻访问的接口
- String url = request.getRequestURI();
- if (url.contains(".") || url.startsWith("/auth/")){
- return true;
- }
- //未登录的用户
- if(null == request.getSession().getAttribute("current_user")){
- response.setCharacterEncoding("UTF-8");
- response.setHeader("content-type","text/html;charset=UTF-8");
- response.getWriter().write("请先登录!");
- return false;
- }else {
- //已登录的用户 进行资源的权限校验
- User user = (User)request.getSession().getAttribute("current_user");
- if(url.startsWith("/apple") && this.hasPermission("apple",user.getRoles())){
- return true;
- }else if (url.startsWith("/banana") && this.hasPermission("banana",user.getRoles())){
- return true;
- }else {
- response.setCharacterEncoding("UTF-8");
- response.setHeader("content-type","text/html;charset=UTF-8");
- response.getWriter().write("暂无权限");
- return false;
- }
-
- }
- }
-
- public boolean hasPermission(String type,List<Role> roles){
- for (Role r : roles){
- if(r.getResources().stream().filter(e -> e.getResourceName().equals(type)).count() > 0){
- return true;
- }
- }
- return false;
- }
- }
至此将对没登录,登陆后不同用户根据权限进行接口访问限制
admin登录
apple登录
未登录访问http://127.0.0.1:5555/banana
至此一个简单的基于RBAC模型的例子完成,下一步结合Spring Security来优化完善。
pom
创建项目,新增依赖
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-thymeleaf</artifactId>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
yml
Spring Security不需要配置即可直接启动
- server:
- port: 5556
-
- spring:
- application:
- name: spring-security-demo
- thymeleaf:
- prefix: classpath:templates/
- suffix: .html
启动类加上@EnableWebSecurity
controller
同上一个例子一样的苹果,香蕉资源
- @RestController
- @RequestMapping("/apple")
- public class AppleController {
- @GetMapping
- public String getApple(){
- return "资源1苹果";
- }
- }
-
- @RestController
- @RequestMapping("/banana")
- public class BnanaController {
- @GetMapping
- public String getBanana(){
- return "资源2香蕉";
- }
- }
此时启动项目,访问这两个接口,均跳转Spring Security自带的登录界面,输入user,密码为控制台日志打印的【Using generated security password】即可登录
修改建应用上下文配置,创建默认用户及权限
通过自定义注入UserDetailsService 用于管理系统的用户账号密码信息,如果不注入,springt security将默认注入一个包含登录名为user的用户(如上),密码打印在控制台
PasswordEncoder密码解析器
spring security要求容器内需要拥有一个PasswordEncoder实例
常用BCryptPasswordEncoder实现类,同一个字符串每次加密得到不同结果
构成:
$2a表示版本 $10 表示算法强度 $xxx 随机盐 xxxx 文本hash值
60位字符串
- @Configuration
- public class MyWebAppConfigurer implements WebMvcConfigurer {
-
- //启动界面的简单配置
- @Override
- public void addViewControllers(ViewControllerRegistry registry) {
- //此处修改为重定向至 /login接口 /login由spring security提供
- registry.addViewController("/").setViewName("redirect:/login");
- }
-
- //免密解析器
- @Bean
- public PasswordEncoder passwordEncoder(){
- return new BCryptPasswordEncoder(10);
- }
-
- /**
- * 设置初始用户来源
- * 自行注入一个UserDetailsService UserDetailsServiceAutoConfiguration中有默认的
- * InMemoryUserDetailsManager
- */
- @Bean
- public UserDetailsService userDetailsService(){
- InMemoryUserDetailsManager manager =new InMemoryUserDetailsManager(
- User.withUsername("admin").password(passwordEncoder().encode("admin")).authorities("apple","salary","ROLE_stu").build(),
- User.withUsername("apple").password(passwordEncoder().encode("apple")). authorities("apple").build(),
- User.withUsername("banana").password(passwordEncoder().encode("banana")).authorities("banana").build());
- return manager;
- }
- }
其中,在设置权限时候,用“ROLE_” + 角色表示角色权限,此时ROLE_不能省略
如配置了角色ROLE_stu,在spring security配置文件中,设置角色权限
.antMatchers("/test/**").hasRole("stu")即可,此时也不能带上"ROLE_"
配置WebSecurityConfig配置
- @EnableWebSecurity
- public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
- //定义安全拦截策略
- @Override
- protected void configure(HttpSecurity httpSecurity) throws Exception{
- httpSecurity.csrf().disable() //关闭csrf跨站攻击防御
- .authorizeRequests()
- .antMatchers("/apple/**").hasAuthority("apple") // 配置拦截路径及资源权限
- .antMatchers("/banana/**").hasAuthority("banana")
- .antMatchers("/auth/**").permitAll() //登录相关接口不进行拦截
- .anyRequest().authenticated() //其他请求都需要登录
- .and()
- .formLogin().defaultSuccessUrl("/auth/success").failureForwardUrl("/auth/failed"); //登录成功/失败跳转的页面
- }
-
- }
修改登录controller
- @Controller
- @RequestMapping("/auth")
- public class LoginController {
-
- //登录成功失败界面
- @GetMapping("/success")
- public String success(){
- return "success";
- }
- @PostMapping("/failed")
- public String failed(){
- return "failed";
- }
-
- //首页
- @RequestMapping("/index")
- public String index(){
- return "index";
- }
-
-
- //获取当前用户
- @PostMapping("/getCurrUser")
- @ResponseBody
- public Object getCurrentUser(HttpSession session){
- return session.getAttribute("current_user");
- }
-
- //登出
- @PostMapping("logout")
- @ResponseBody
- public ResponseEntity logout(HttpSession session){
- session.removeAttribute("current_user");
- return ResponseEntity.ok("已退出登录");
- }
-
- /**
- * 获取当前用户的多种方式
- * @param principal
- * @return
- */
- @GetMapping("/getUserByPrincipal")
- public String getUserByPrincipal(Principal principal){
- return principal.getName();
- }
- @GetMapping(value = "/getLoginUserByAuthentication")
- public String currentUserName(Authentication authentication) {
- return authentication.getName();
- }
- @GetMapping(value = "/username")
- public String currentUserNameSimple(HttpServletRequest request) {
- Principal principal = request.getUserPrincipal(); return principal.getName();
- }
- // @GetMapping("/getLoginUser")
- // public String getLoginUser(){
- // User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal() ;
- // return user.getUsername();
- // }
-
- }
将首页/登录成功页/失败页复制过来
修改首页中获取当前用户部分
- function currentUser() {
- $.ajax({
- type : "GET",
- url : "/auth/getUserByPrincipal",
- data : {},
- success : function (result) {
- result = JSON.stringify(result);
- $('#currentUser').html("success:" + result);
- },
- error : function (error) {
- $('#currentUser').html("error:" + error);
- }
- })
- }
至此简单的spring security项目准备完毕,但是此时用户信息及权限规则,均通过UserDetailsService写至内存中,实际项目需要结合数据库来获取用户权限数据
启动项目-》登录前,直接请求/banana接口,发现均会跳转至自带登录界面,用banana/banana登录后,正常获取banana资源,而获取不到apple资源
直接访问http://127.0.0.1:5556/banana
正常获取数据,访问http://127.0.0.1:5556/apple
返回403无法访问
使用自带/logout接口退出当前用户后,访问/banana接口,则此时跳转至登录界面
SpringSecurity通过引用Spring容器的UserDetailsService来管理用户数据,默认情况下会提供一个为user的用户,可通过自定义注入改变用户数据,但实际开发中,用户数据及权限配置信息来源于数据库。在SpringSecurity提供JdbcUserDetailsManager对数据库的用户数据进行管理
自定义登录界面整合数据库权限管理
修改配置从数据库获取用户及权限
修改UserDetails,从数据库加载用户信息
修改WebSecurityConfig中httpSecurity,改成从数据库加载授权配置
SpringSecurity提供多种密码解析器,CryptPassEncoder/Argon2PasswordEncoder等,接实现PassEncoder接口,最常见为BCryptPasswordEncoder
自定义拦截配置
配置类WebSecurityConfig继承WebSecurityConfigurerAdapter,通过重写的configure(HttpSecurity httpSecurity)进行拦截规则配置,包括访问控制,登录登出界面设置等
自定义登录
通过配置HttpSecurity中的方法来自定义
loginPage()配置登录页面
如在WebSecurityConfig中新增.loginPage("/auth/index")
当输入http://127.0.0.1:5556/login
页面跳转为自定义界面 http://127.0.0.1:5556/auth/index
此时需要修改登录界面index.html 及登录请求接口的逻辑
修改index.html中action 编写/auth/tologin接口用于处理登录逻辑,自定义后,将不会使用UserDetailsService来进行处理
- <form action="/auth/tologin" method="post">
- 用户名:<input type="text" name="userName" required><br/>
- 密 码:<input type="text" name="password" required><br/>
- <input type="submit" value="登录"> <input type="reset" value="重置">
- </form>
loginProcessingUrl() 设置登录逻辑
登录时提交remember-me参数 on/yes/1/true 则会记住当前用户的token至cookie中(默认失效时间2周.tokenValiditySeconds(60)设置失效时间)
httpSecurity.rememberMe().rememberMeParameter("remember-me") //登录时,前端传参的名称
httpSecurity..authorizeRequests().antMachers()设置请求路径匹配
antMachers().permitAll()允许所有人访问,
antMachers().denyAll()所有人拒绝访问,
antMachers().anonymous()未登录可以访问,已登录不可访问
antMachers().hasAuthority()/hasRole() 配置需要有对应的权限/角色才能访问
Cross-Site Request Forgery 跨站点请求伪造,一种安全攻击手段,利用客户端信息伪装成正常用户进行攻击。
当打开csrf配置后,SpringSecurity提供CsrfFilter对csrf参数进行检查,每次请求,session中加入_csrf的token,每次带上token,SpringSecurity进行检查
@ControllerAdvice注入一个异常处
理类,以@ExceptionHandler注解声明方法,往前端推送异常信息。
权限控制注解
以下四个注解默认不生效,需要在配置类(MyWebAppConfigurer)或者启动类加上@EnableGlobalMethodSecurity(prePostEnabled = true)开启
@PreAuthorize("hashRole(‘admin’)") 执行方法前判断是都具有该角色权限
注解支持ROLE_admin/admin均可,但是配置类中.antMatchers("/test/**").hasRole("stu")不能带上"ROLE_"
@PostAuthorize("returnObject.name == authentication.name") 方法执行后,判断返回的值是否与认证主体中的某个值相等,若相等则返回,否则抛出异常
@PreFilter(filterTarget=”a“, value=”filterObject%2==0“) 方法执行前,过滤参数a 当a%2不等于0则会被过滤,不会传入方法中(入参a 为List<Integer> a)
@PostFilter("filterObject.name == authentication.name“) 方法执行后根据条件过滤结果
角色控制注解
@EnableGlobalMethodSecurity(securedEnable = true)开启。
@Secured("ROLE_stu") 作用于方法,类,判断是否具备该角色(角色严格区分大小写)
通过认证后,SpringSecurity提供会话管理,保存用户的认证信息,以避免每次操作都要进行认证操作。认证通过后,身份信息将放入SecurityContextHolder上下文,SecurityContext与当前线程绑定,用于方便获取用户身份
通过SecurityContextHolder.getContext().getAuthentication()获取当前用户登录信息
- /**
- * 获取当前用户的多种方式
- * @param principal
- * @return
- */
- @GetMapping("/getUserByPrincipal")
- @ResponseBody
- public String getUserByPrincipal(Principal principal){
- return principal.getName();
- }
-
- @GetMapping(value = "/getLoginUserByAuthentication")
- @ResponseBody
- public String currentUserName(Authentication authentication) {
- return authentication.getName();
- }
-
- @GetMapping(value = "/username")
- @ResponseBody
- public String currentUserNameSimple(HttpServletRequest request) {
- Principal principal = request.getUserPrincipal(); return principal.getName();
- }
-
- @GetMapping(value = "/username")
- @ResponseBody
- public String getUserByContext() {
- Principal principal = (Principal)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
- return principal.getName();
- }
通过配置sessionCreationPolicy参数来管理Session
- @EnableWebSecurity
- public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
- //定义安全拦截策略
- @Override
- protected void configure(HttpSecurity httpSecurity) throws Exception{
- httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
- .and()
- ......
策略包含
always 如果无Session则创建一个
if required 如果需要登录时创建一个(默认)
never SpringSecurity不会创建Session,如果应用其他地方创建,则会被使用
stateless SpringSecurity绝不创建也不使用Session
在yml文件中直接设置session过期时间
server.servlet.session.timeout = 3000s (springsecurity默认一分钟 小于一分钟也会按照一分钟失效处理)
session过期后可通过SpringSecurity设置跳转界面
httpSecurity . sessionManagement (). expiredUrl ( "/auth/index ?error=EXPIRED_SESSION" ) //session过期后跳转. invalidSessionUrl ( "/auth/index ?error=INVALID_SESSION" ); //传入无效sessionId跳转
同账号多端登录前者被踢下线功能
httpSecurity.sessionManagement()
.maximumSessions(1) //设置某段时间内允许几个端登录
.maxSessionsPreventsLogin(false) //true表示已经登录则不允许再次登录 false允许再次登录但前者会被踢下线
.expiredSessionStrategy(new CustomExpiredSessionStrategy()) //自定义被踢下线后的操作 需要实现自定义策略
public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy{
private ObjectMapper objectMapper = new ObjectMapper();
@override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event){
Map<String, Object> map = new HashMap();
map.put("code",403);
map.put("msg","您已在其他地方登录,被迫下线" + event.getSessionInformation().getLsatRequest());
String json = objectMapper.writeValueAsString(map);
event.getResponse().setContentType("application/json;charset=utf-8");
event.getResponse().getWriter().write(json);
}
}
通过两个不同的浏览器进行同账号登录测试
在yml文件中直接设置
server.servlet.session.cookie.http ‐ only = true # true 浏览器脚本无法访问cookieserver.servlet.session.cookie.secure = true # true cookie仅通过Https发送
SpringSecurtity提供退出接口/logout 跳转至登出界面,可以直接调用
也可在WebSecurityConfig中自定义退出界面/退出后跳转地址
- .and()
- .logout() //开启自定义退出,使用WebSecurityConfigurerAdapter 会自动被应用 .logoutUrl("/logout") //默认退出地址
- .logoutSuccessUrl("/auth/logout") //退出后的跳转地址
- .addLogoutHandler(new SecurityContextLogoutHandler()) //添加LogoutHandler,负责退出时的清理工作.默认 SecurityContextLogoutHandler会被添加为最后一个LogoutHandler
- .invalidateHttpSession(true); //指定是否在退出时让HttpSession失效,默认是true
退出时,会进行的操作(SecurityContextLogoutHandler)
1.http session失效
2.清除SecurityContext上下文
3.跳转至退出后的地址
SecurityContextLogoutHandler退出时负责SecurityContext的清理工作
单体系统演变至多服务,多个服务利用一套独立的第三方系统提供统一的认证授权
统一认证授权
独立的认证服务,用于统一处理认证授权,不管是不同类型的用户,不同类型的客户端(web,h5,小程序,app等),采用一致的认证授权会话处理机制,实现统一登录认证授权
需要支持多种认证方式:账号密码,短信验证,二维码,人脸识别等灵活切换
多重认证场景
购物,支付需要不同安全等级需要对应认证场景
应用接入认证
开放部分API给第三方使用。内部与外部第三方采用统一的
认证方案(基于session与token两种方案)
方案1基于session的认证方式
在分布式中,将session信息同步至各个服务,并对请求进行负载均衡
做法:
1.进行session复制:多个服务器之间同步session,使得session保持一致,但是对外透明
2.session黏贴:用户访问服务器集群中某台服务器后,后续所有请求都需要落到该服务器上
3.session集中存储:将session存入分布式缓存中,所有服务器均从分布式缓存中获取session信息
优点:更好的在服务端进行会话控制,安全性较高
缺点:但是客户端需要存储sessionId,对不同的客户端不能有效使用等
方案2基于token的认证方式
生成认证的token存储,每次请求携带并进行校验
选择方案
统一认证服务(UAA)和网关结合来进行认证授权
uaa负责接入方认证,登录用户认证,授权,令牌管理,完成实际的用户认证授权
API网关作为整个分布式系统的统一入口,进行身份认证,监控,负载均衡等
OAuth开放授权标准,允许用户授权第三方应用B获取在微信上注册的信息,从而不需要向B提供微信账号密码,避免B获取用户在微信上的所有数据内容。OAuth协议用于保证双方的可信。
用户在B上选择通过微信登录-》弹出微信登录方式(账号密码/二维码等)-》选择弹出的微信同意登录-》B获取用户微信账号信息-》B新建账号与微信绑定-》用B账号进行登录
一个应用要求通过OAuth授权,需要现在对方网站进行登记,这样在请求的时候对方才能知道谁在请求
客户端Client:如浏览器,微信客户端,不存储资源,需要通过资源拥有者授权去请求资源服务器的资源
资源拥有者Resource Owner:用户,也可以是应用程序,表示某个资源的拥有者
授权服务器Authorization Server:认证服务器,如微信,服务提供者对资管拥有者的身份认证,对访问资源进行授权,认证成功给客户端发放令牌(access_token)作为凭证
资源服务器Resource Server:如微信服务器,B服务器,通过OAuth协议让B获取用户在微信上的用户信息,B同时通过OAuth协议让用户访问自己的资源
clientDetails:cilent_id客户信息,代表B在微信中的唯一索引secret :密钥,B获取微信的信息需要提供加密字段scope:授权作用域,代表B获取微信的信息访问access_token:授权码,B获取微信用户信息的凭证,如微信的接口调用凭证grant_type:授权类型,如微信支持基于授权码的authorization_code模式,OAuth提供多种授权方式userDetails:授权用户标识user_id,如微用户在微信中微信号
AuthorizationEndpoint 服务用于认证请求。默认URL:/oauth/authorizeTokenEndpoint 服务用于访问令牌的请求。默认URL:/oauth/tokenOAuth2AuthenticationProcessingFilter 用于对请求方给出的身份令牌进行解析鉴权
1.客户请求server-uaa服务申请access_token授权码
2.客户携带access_token访问server-b服务
3.server-b校验access_token合法性,若合法返回资源信息
描述:通过码云实现模拟第三方登录(授权码模式)
1.新建简单项目,创建个简单回调页面(客户端)
2.在码云上注册应用,获取Client ID(客户端id),Client Secret(密钥)等(授权服务器,码云同为资源服务器)
上面说过一个应用要求通过OAuth授权,需要现在对方网站进行登记,这样在请求的时候对方才能知道谁在请求,所以此处创建第三应用可以理解为,在码云上登记一个客户端信息
3.点击模拟请求进行测试-》同意授权
4.页面跳转至我们设置的回调界面,在链接上带回一个code
5.查看码云OAuth文档
Gitee OAuth 文档https://gitee.com/api/v5/oauth_doc
请求的几个参数
1.clientDetails(client_id) 客户信息,第三方应用id,项目在码云中的唯一索引
2.secret 密钥,代表获取码云信息需要提供的加密字段(与码云加密算法有关)
3.scope 授权作用域,可以获取码云信息的范围
4.access_token 授权码,允许获取码云信息的凭证
5.grant_type 四种授权类型
1.通过请求,进行授权模拟(授权码模式)
.根据文档,通过浏览器将用户引导至码云第三方认证页面(GET)
https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code
带入实际参数client_id,redirect_uri,response_type
https://gitee.com/oauth/authorize?client_id=c60b65bee5e46c7835ef999e9726b81b18e0ae69e39f25e80ede4df45355278f&redirect_uri=http://127.0.0.1:8888/callback.html&response_type=code
2.出现第3步,第四步相同操作
3. 将授权码code向码云认证服务器发送POST请求,获取access_token(有效期一天)
https://gitee.com/oauth/token?grant_type=authorization_code&code=c59e19acf5eb78211ca4246d21cd652cbba0e3124d86c7d9ea4deeecd1e9d513&client_id=c60b65bee5e46c7835ef999e9726b81b18e0ae69e39f25e80ede4df45355278f&redirect_uri=http://127.0.0.1:8888/callback.html&client_secret=9548b96604a5ab1a50b201c09065a2e0c84e95be3c4163704ee29341080afcd5
当access_token失效后,可以通过refresh_token重新获取access_token(POST)
https://gitee.com/oauth/token?grant_type=refresh_token&refresh_token={refresh_token}
1.客户端:例子中创建的客户端服务,本身无资源,通过浏览器去获取码云资源,及需要通过资源拥有者的授权去资源服务器获取资源
2.资源拥有者,例子中拥有码云账号的用户,资源拥有者
3.授权服务器(认证服务器),例子中码云认证服务器,对资源拥有者身份认证授权,认证成功发放令牌(access_token)作为客户端访问资源服务器凭证
4.资源服务器,例子中码云,存储资源的云服务器,通过OAuth协议,用户可以获取码云账户数据,仓库数据,代码等资源
用户访问客户端,客户端向授权服务器发起授权
授权服务器,引导用户进入授权界面,等待用户同意授权
用户同意授权
回调客户端界面,带回授权码
客户端通过授权码向授权服务器请求获取令牌access_token
适用:安全性最高,最常用的流程
授权码模式简化
与授权码模式相似,但是用户同意授权后直接返回access_token
适用:纯前端项目
客户端与资源所有者高度可信情况下可用,如客户端为系统的一部分
资源拥有者直接提供账号密码给客户端,向授权服务器申请令牌access_token
适用:高度信任的情况下
该情况适用于无前端的纯后台项目,客户端向授权服务器发送身份信息,请求access_token
适用:纯后端项目
本例资源服务,认证服务均为同一个服务,采用客户端模式(client_credentials)
- server:
- port: 8888
-
- spring:
- application:
- name: oauth2-demo
引入安全认证依赖,引入后,springboot便会对资源服务器上所有资源进行默认的保护
<dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
1.token endpoint安全束缚配置
主要配置允许客户端以Form表单形式登录/配置密码加密方式等)
2.客户端详情设置
客户端详情包括(client_id,client_secret,grant_type,scope)
剋设置客户端详情存储位置,内存或者数据库
3.配置授权,token endpoint,令牌服务
本例采用客户端模式
- @Configuration
- @EnableAuthorizationServer
- public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
-
- //配置采用的加密方式
- @Bean
- public PasswordEncoder passwordEncoder(){
- return new BCryptPasswordEncoder();
- }
-
-
- //配置安全约束 定义令牌终结点上的安全约束
- @Override
- public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
- //允许客户端以form表单登录,如微信获取access token
- security.allowFormAuthenticationForClients();
- }
-
- //配置客户端详细信息
- @Override
- public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
- clients.inMemory()
- //client_id
- .withClient("client")
- //授权方式 "authorization_code", "password", "client_credentials", "implicit", "refresh_token"
- .authorizedGrantTypes("client_credentials")
- // 授权范围 all表示所有,write等
- .scopes("all")
- // client_secret配置加密类型,如果不需要 则直接 .secret("{noop}123456"); {noop表示空操作}
- .secret(new BCryptPasswordEncoder().encode("123456"));
-
- }
-
- //令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
- @Override
- public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
- super.configure(endpoints);
- }
-
- }
1.资源服务器安全配置
2.http安全配置,保护资源API
配置类
- @Configuration
- @EnableResourceServer
- public class MyResourceServerConfigurer extends ResourceServerConfigurerAdapter {
- @Override
- public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
- super.configure(resources);
- }
-
- @Override
- public void configure(HttpSecurity http) throws Exception {
- http.authorizeRequests()
- .antMatchers("/apple/**").authenticated();
- }
- }
资源请求API
- @RestController
- @RequestMapping("/apple")
- public class AppleController {
- @GetMapping
- public String getApple(){
- return "资源1苹果";
- }
- }
至此简单的认证服务与资源服务配置完成,本例认证服务与资源服务为同一个项目
未授权进行资源的请求,此时报错,提示未授权
获取token
SpringBoot OAuth 默认获取token的endpoint路劲为 /oauth/token
其中expires_in 为失效时间(秒),每次重复请求,返回同一个token,但是失效时间再减少
带上token进行请求,得到结果
目的:将客户端Client信息与token存储至数据库(token一般存放redis)
本例中用到postgre sql
- create table oauth_client_details (
- client_id VARCHAR(256) PRIMARY KEY, -- 客户端id
- resource_ids VARCHAR(256) , -- 资源id集合,英文逗号隔开
- client_secret VARCHAR(256) , -- 客户端密钥
- scope VARCHAR(256) , -- 授权范围
- authorized_grant_types VARCHAR(256) , -- 授权类型 authorization_code,password,refresh_token,implicit,client_credentials,英文逗号隔开
- web_server_redirect_uri VARCHAR(256), -- 客户端重定向uri
- authorities VARCHAR(256), -- 客户端拥有spring security权限值
- access_token_validity INTEGER , -- token有效时间 秒 默认12小时
- refresh_token_validity INTEGER , -- refresh_token有效时间 默认30天
- additional_information VARCHAR(4096) , -- 预留json字段
- autoapprove VARCHAR(256) -- 是否启动自动approve操作,适用于授权码模式authorization_code
- );
-
- create table oauth_access_token (
- token_id VARCHAR(256) , -- MD5算法加密后的access_token
- token bytea, -- access_token序列化二进制数据格式 mysql中为BLOB类型
- authentication_id VARCHAR(256) PRIMARY KEY , -- 根据username,client_id,scopeMD5加密生成的主键
- user_name VARCHAR(256), -- 用户名
- client_id VARCHAR(256), -- 客户端id
- authentication bytea, -- OAuth2Authentication对象序列化后的二进制数
- refresh_token VARCHAR(256) -- refresh_token MD5加密后的值
- );
官方完整建表sql
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
注意此处密码应该进行BCrypt加密
123456 对应
$2a$10$FB98FkLGqA2sBEURWp4un.sW2VCSKd7NjT2o2GK/4njggHkEeYaZu
INSERT INTO oauth_client_details (client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('client', 'apple', '$2a$10$FB98FkLGqA2sBEURWp4un.sW2VCSKd7NjT2o2GK/4njggHkEeYaZu', 'all', 'client_credentials', null, null, null, null, null, null);
pom文件新增对应数据库的支持依赖
<dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency>
yml
- server:
- port: 8888
-
- spring:
- application:
- name: oauth2-demo
- datasource:
- # 数据库引擎
- driver-class-name: org.postgresql.Driver
- # 数据库地址 characterEncoding防止出现中文乱码 若为https则无需加useSSL
- url: jdbc:postgresql://127.0.0.1:5432/test?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&yearIsDateType=false&stringtype=unspecified
- # 用户
- username: postgres
- # 密码
- password: 123456
- # 数据库连接类型
- # 时区
- jackson:
- time-zone: GMT+8
给资源配置resourceId
- @Override
- public void configure(ResourceServerSecurityConfigurer resources) throws Exception
- //super.configure(resources);
- resources.resourceId("apple");
- }
从原来从内存中获取客户端信息改为从数据库获得客户端信息
- @Configuration
- @EnableAuthorizationServer
- public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
-
- private final DataSource dataSource;
-
- public MyAuthorizationConfig(DataSource dataSource) {
- this.dataSource = dataSource;
- }
-
- //配置采用的加密方式
- @Bean
- public PasswordEncoder passwordEncoder(){
- return new BCryptPasswordEncoder();
- }
-
- @Override
- public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
- clients.jdbc(dataSource);
- }
-
- //配置安全约束 定义令牌终结点上的安全约束
- @Override
- public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
- //允许客户端以form表单登录,如微信获取access token
- security.allowFormAuthenticationForClients();
- }
-
- //令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
- @Override
- public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
- endpoints.tokenStore(new JdbcTokenStore(dataSource));
- }
- }
请求测试
每次请求,OAuth会去客户端信息表oauth_client_details查询当前发送过来的客户信息是否正确,认证成功后将返回令牌access_token,同时由于设置了将令牌存储至数据库,此时oauth_access_token也会新增一条数据
postman认证成功返回令牌
成功获取资源服务的资源
至此OAuth2 客户端授权模式结合数据库存储的简单例子结束,本例中创建了两张需要的表,一为客户端详情表,代替之前在代码中将客户端信息写入内存的方式,二是令牌token表,用于存储token,但实际开发中,token一般不存储在数据库中,不然每次请求接口都需要访问数据库
上面的例子已经设计将token存在内存,数据库中,但更合理的应该存在redis上,且redis可设置数据的有效期。这样可以避免高并发情况下频繁访问数据库。在分布式架构中,将token存在其中一台服务器实例的内存中也不合适。
pom新增redis依赖
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
yml配置文件新增redis配置
redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 本地localhost/127.0.0.1 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # 连接超时时长(毫秒) timeout: 6000 # Redis服务器连接密码(默认为空) password:
认证服务MyAuthorizationConfig修改
令牌访问端点修改为redis存储方式
- @Resource
- private RedisConnectionFactory redisConnectionFactory;
-
- ......
-
- //令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
- @Override
- public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
- endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory));
- //endpoints.tokenStore(new JdbcTokenStore(dataSource));
- }
启动本地redis,启动服务请求获得token,此时redis中
JWT简单介绍
JSON Web Token(JWT),客户端服务器通过JWT规定格式进行身份认证完成交互,JWT分三段,每段通过.隔开,包含不同信息分别为
Header头部:JSON数据Base64编码,包含加密类型等
PayLoad负载:JSON数据Base64编码,包含客户端授权信息
Signature签名:头部+负载+密钥(盐)通过加密算法获得,防止数据篡改(盐存在服务端,保密)
导入依赖
- <dependency>
- <groupId>org.springframework.security</groupId>
- <artifactId>spring-security-jwt</artifactId>
- <version>1.1.1.RELEASE</version>
- </dependency>
修改认证服务配置
- //设置密钥串
- private static final String SIGNING_KEY = "oauth_test";
-
- ......
-
- //令牌访问端点配置 可以完成令牌服务以及令牌服务各个endpoint配置
- @Override
- public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
- //设置token转换器
- JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
- //设置加签密钥
- jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
- //设置校验器
- jwtAccessTokenConverter.setVerifier(new MacSigner(SIGNING_KEY));
- TokenStore tokenStore = new JwtTokenStore(jwtAccessTokenConverter);
- endpoints.accessTokenConverter(jwtAccessTokenConverter);
- endpoints.tokenStore(tokenStore);
- }
启动服务请求获得token,并成功请求资源接口
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。