上一篇文章中基于SpringSecurity已经有了完整的详解,今天这篇文章基于SpringSecurity将Oauth2整合进去。
1. OAuth2是什么
简单来说,就是一种授权认证;一种针对开放系统间授权,分布式访问(单点登录)和现代微服务安全的解决方案;这种解决方案为我们提供了一种思路,但具体的实现我们要自己书写(就好比给我们一个接口)
所能解决的问题:开放系统键授权,分布式访问(单点登录)和 现代微服务安全
项目依赖:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<!-- jwt增强-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
一:创建所需要的User类,Menu权限类,WebUtils响应工具类(用于异常处理器中),SysResult响应数据工具类
代码如下:
User类
package com.jt.pojo;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* 用户表(User)实体类
*
* @author
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_user")
public class User implements Serializable {
private static final long serialVersionUID = -40356785423868312L;
/**
* 主键
*/
@TableId
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
/**
* 密码
*/
private String password;
/**
* 账号状态(0正常 1停用)
*/
private String status;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phonenumber;
/**
* 用户性别(0男,1女,2未知)
*/
private String sex;
/**
* 头像
*/
private String avatar;
/**
* 用户类型(0管理员,1普通用户)
*/
private String userType;
/**
* 创建人的用户id
*/
private Long createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 删除标志(0代表未删除,1代表已删除)
*/
private Integer delFlag;
}
权限类
package com.jt.pojo;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* 菜单表(Menu)实体类
*
* @author makejava
* @since 2021-11-24 15:30:08
*/
@TableName(value="sys_menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {
private static final long serialVersionUID = -54979041104113736L;
@TableId
private Long id;
/**
* 菜单名
*/
private String menuName;
/**
* 路由地址
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 菜单状态(0显示 1隐藏)
*/
private String visible;
/**
* 菜单状态(0正常 1停用)
*/
private String status;
/**
* 权限标识
*/
private String perms;
/**
* 菜单图标
*/
private String icon;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
/**
* 是否删除(0未删除 1已删除)
*/
private Integer delFlag;
/**
* 备注
*/
private String remark;
}
SysResult类
package com.jt.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class SysResult implements Serializable {
private Integer status; //200业务执行成功 201业务执行失败
private String msg; //服务器的提示信息
private Object data; //封装后台返回值
public static SysResult fail(){
return new SysResult(201,"业务执行失败",null);
}
public static SysResult success(){
return new SysResult(200,"业务执行成功",null);
}
//服务器返回业务数据
public static SysResult success(Object data){
return new SysResult(200,"业务执行成功",data);
}
public static SysResult success(String msg,Object data){
return new SysResult(200,msg,data);
}
}
WebUtils工具类
package com.jt.util;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 往响应当中写入数据的工具类
*/
public class WebUtils
{
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}
二:实现UserDetailsService类,进行实现自己数据库获取用户信息和权限
创建Mapper,编译xml进行获取用户的信息和权限:
UserMapper,使用Mybatis-plus
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
MenuMapper
@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
List<String> selectPermsByUserId(String userId);
}
MenuMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.jt.mapper.MenuMapper">
<select id="selectPermsByUserId" resultType="java.lang.String">
SELECT
DISTINCT m.`perms`
FROM
sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE
user_id = #{userId}
AND r.`status` = 0
AND m.`status` = 0
</select>
</mapper>
实现UserDetails,用此对象封装用户信息和权限
package com.jt.pojo;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 此方法实现的是UserServiceImpl 中的对象 封装了用户的信息
*/
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
private List<String> permissions;
@JSONField(serialize = false) //因为下面的泛型序列化可能会出错,所有用此注解让他不进行序列化
private List<GrantedAuthority> authorities;//方便下面调用进行创建静态的
@Override //此方法是获取权限信息的 所以要进行重写
public Collection<? extends GrantedAuthority> getAuthorities() {
//把permission 中 string 类型的权限封封装成 SimpleGrantedAuthority对象
if (authorities!=null){ //此处进行判断一下,因为每次调用如果不为空则权限已经有
return authorities;
}
authorities = new ArrayList<>();
for (String permission : permissions) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true; //更改成了true
}
@Override
public boolean isAccountNonLocked() {
return true; //更改成了true
}
@Override
public boolean isCredentialsNonExpired() {
return true; //更改成了true
}
@Override
public boolean isEnabled() {
return true; //更改成了true
}
}
实现UserDetailsService进行设定自己数据库账号密码
package com.jt.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jt.mapper.MenuMapper;
import com.jt.mapper.UserMapper;
import com.jt.pojo.LoginUser;
import com.jt.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
@Override //设定成自己数据库账号密码 获取用户的信息和权限
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(queryWrapper);
//如果没有查询到用户就抛出异常
if(Objects.isNull(user)){
throw new RuntimeException("用户名或者密码错误");
}
//TODO 获取权限
String s = user.getId().toString();
List<String> list = menuMapper.selectPermsByUserId(s);
//把数据封装成UserDetails返回
return new LoginUser(user,list);
}
}
三:定义Security配置类
定义Spring Security配置类,在此类中配置认证规则,例如:
package com.jt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//创建BCryptPasswordEncoder 注入容器 加密方式
@Bean
public BCryptPasswordEncoder passwordEncoder(){
//当你将此对象注入容器时,就会自动将密码进行bc的比对校验。
//如果输入的明文密码与数据库中的加密密码不匹配则报错。
//切数据库中必须存储为bc加密的密码
return new BCryptPasswordEncoder();
}
/**
* 定义认证管理器对象,这个对象负责完成用户信息的认证,
* 即判定用户身份信息的合法性,在基于oauth2协议完成认
* 证时,需要此对象,所以这里讲此对象拿出来交给spring管理
* @return
* @throws Exception
*/
@Bean
@Override //需要通过AuthenticationManager的authenticate方法进行用户认证,所有需要在此将其注入容器
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
BC注入及Manager在另一个SpringSecurity有讲解。
四:构建令牌生成及配置对象
本次我们借助JWT(Json Web Token-是一种json格式)方式将用户相关信息进行组织和加密,并作为响应令牌(Token),从服务端响应到客户端,客户端接收到这个JWT令牌之后,将其保存在客户端(例如localStorage),然后携带令牌访问资源服务器,资源服务器获取并解析令牌的合法性,基于解析结果判定是否允许用户访问资源.
package com.jt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/**
* 在此配置类中配置令牌的生成,存储策略,验签方式(令牌合法性)。
*/
@Configuration
public class TokenConfig {
/**
* 配置令牌的存储策略,对于oauth2规范中提供了这样的几种策略
* 1)JdbcTokenStore(这里是要将token存储到关系型数据库)
* 2)RedisTokenStore(这是要将token存储到redis数据库-key/value)
* 3)JwtTokenStore(这里是将产生的token信息存储客户端,并且token
* 中可以以自包含的形式存储一些用户信息)
* 4)....
*/
@Bean
public TokenStore tokenStore(){
//这里采用JWT方式生成和存储令牌信息
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 配置令牌的创建及验签方式
* 基于此对象创建的令牌信息会封装到OAuth2AccessToken类型的对象中
* 然后再存储到TokenStore对象,外界需要时,会从tokenStore进行获取。
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter=
new JwtAccessTokenConverter();
//JWT令牌构成:header(签名算法,令牌类型),payload(数据部分),Signing(签名)
//这里的签名可以简单理解为加密,加密时会使用header中算法以及我们自己提供的密钥,
//这里加密的目的是为了防止令牌被篡改。(这里密钥要保管好,要存储在服务端)
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//设置密钥
return jwtAccessTokenConverter;
}
/**
* JWT 令牌签名时使用的密钥(可以理解为盐值加密中的盐)
* 1)生成的令牌需要这个密钥进行签名
* 2)获取的令牌需要使用这个密钥进行验签(校验令牌合法性,是否被篡改过)
*/
private static final String SIGNING_KEY="auth";
}
五:定义Oauth2认证授权配置
Oauth2配置
package com.jt.config;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import java.util.UUID;
/**
* Oauth2 是一种认证授权规范,它基于认证和授权定义了一套规则,在这套规则中规定了
* 实现一套认证授权系统需要哪些对象:
* 1)系统资源(数据)
* 2)资源拥有者(用户)
* 3)管理资源的服务器
* 4)对用户进行认证和授权的服务器
* 5)客户端系统(负责提交用户身份信息的系统)
*
* 思考:对于一个认证授权系统来讲,需要什么?:
* 1)提供一个认证的入口?(客户端去哪里认证)
* 2)客户端应该携带什么信息去认证?(username,password,....)
* 3)服务端通过谁去对客户端进行认证(一个负责认证的对象)?
*/
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer //在oauth2规范中启动认证和授权
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
//@Autowired
private AuthenticationManager authenticationManager;
//@Autowired
private BCryptPasswordEncoder passwordEncoder;
//@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
//@Autowired
private TokenStore tokenStore;
//提供一个认证的入口(客户端去哪里认证)?(http://ip:port/.....)
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//super.configure(security);
//对外发布认证入口(/oauth/token),认证通过服务端会生成一个令牌
security.tokenKeyAccess("permitAll()")
//对外发布检查令牌的入口(/oauth/check_token)
.checkTokenAccess("permitAll()")
//允许用户通过表单方式提交认证,完成认证
.allowFormAuthenticationForClients();
}
//定义客户端应该携带什么信息去认证?
//指明哪些对象可以到这里进行认证(哪个客户端对象需要什么特点)。
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//super.configure(clients);
clients.inMemory()
//客户端标识
.withClient("gateway-client")
//客户端密钥(随意)
.secret(passwordEncoder.encode("123456"))
//指定认证类型(码密,刷新令牌,三方令牌,...)
.authorizedGrantTypes("password","refresh_token")
//作用域(在这里可以理解为只要包含我们规定信息的客户端都可以进行认证)
.scopes("all");
}
//提供一个负责认证授权的对象?(完成客户端认证后会颁发令牌,默认令牌格式是uuid方式的)
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//super.configure(endpoints);
//设置认证授权对象
endpoints.authenticationManager(authenticationManager)
//设置令牌业务对象(此对象提供令牌创建及有效机制设置)
.tokenServices(tokenService())//不写,默认是uuid
//设置允许对哪些请求方式进行认证(默认支支持post):可选
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
}
@Bean
public AuthorizationServerTokenServices tokenService(){
//1.构建token业务对象
DefaultTokenServices ts=new DefaultTokenServices();
//2.设置令牌生成机制(创建令牌的方式,存储用户状态信息的方式)
ts.setTokenStore(tokenStore);
//3.设置令牌增强(改变默认令牌创建方式,没有这句话默认是UUID)
ts.setTokenEnhancer(jwtAccessTokenConverter);
//4.设置令牌有效时长(可选)
ts.setAccessTokenValiditySeconds(3600);
//5.设置刷新令牌以及它的有效时时长(可选)
ts.setSupportRefreshToken(true);
ts.setRefreshTokenValiditySeconds(3600*24);
return ts;
}
}
六:ResourceConfig定义路径设置,异常处理器等
授权失败处理器
package com.jt.config;
import com.alibaba.fastjson.JSON;
import com.jt.util.WebUtils;
import com.jt.vo.SysResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component//授权失败处理器
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
SysResult result = new SysResult().setStatus(403).setMsg("您的权限不足");
String json = JSON.toJSONString(result);
//处理异常
WebUtils.renderString(response,json);
}
}
认证失败处理器
package com.jt.config;
import com.alibaba.fastjson.JSON;
import com.jt.util.WebUtils;
import com.jt.vo.SysResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component //自定义认证失败异常处理器
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
SysResult result = new SysResult().setStatus(401).setMsg("用户认证失败请查询登录");
String json = JSON.toJSONString(result);
//处理异常
WebUtils.renderString(response,json);
}
}
定义ResourceConfig
package com.jt.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@Configuration
@EnableResourceServer
//启动方法上的权限控制,需要授权才可访问的方法上添加@PreAuthorize等相关注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPoint;
@Override
public void configure(HttpSecurity http) throws Exception {
//1.关闭csrf跨域攻击
http.csrf().disable();
//2.放行相关请求
http.authorizeRequests()
.antMatchers("/resource/**")
.authenticated()
.anyRequest().permitAll();
//配置异常处理器
http.exceptionHandling()
//配置认证失败及授权失败处理器
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}
}
七:创建Controller,并用注解的形式测试权限
package com.jt.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/resource")
public class ResourceController {
/**
* 查询资源
* @return
*/
@PreAuthorize("hasAuthority('system:test:list')")
@GetMapping
public String doSelect(){
return "Select Resource ok";
}
/**
* 创建资源
* @return
*/
@PreAuthorize("hasAuthority('system:test:list')")
@PostMapping
public String doCreate(){
return "Create Resource OK";
}
/**
* 修改资源
* @return
*/
@PreAuthorize("hasAuthority('system:test:list')")
@PutMapping
public String doUpdate(){
return "Update Resource OK";
}
/**
* 删除资源
* @return
*/
@DeleteMapping
public String doDelete(){
return "Delete resource ok";
}
}
八:测试
使用Postman进行测试
访问登录接口:localhost:8080/oauth/token -----这个接口是oauth自带的登录接口
输入账号密码和自己定义的规则内容就会调用配置访问自己的数据库并进行验证。
定义参数: 全部都是自己定义的
- grant_type:授权方式
- client_id:客户端的名称
- client_secret:客户端密码
- username:数据库中的账号
- password:数据库中的密码
测试得到的token
访问接口:localhost:8080/oauth/check_token
参数:token 将上面得到token 放入
会得到用户的所有信息,包括用户的权限等
测试权限
访问自己设定的接口:
可以看到使用注解配置的权限中与上面得到的用户中的权限数据一致,则访问成功
Headers参数:Authorization 必须将得到的token放于此Headers中。而且必须token前面加上bearer
测试错误token,或者接口权限不对等
当权限不对等时就会调用我们创建的处理器。
**授权码模式**
上面是通过密码进行登录的模式,现在讲解授权码模式
授权码模式指:通过第三方进行认证授权登录,比如微信,qq,哔哩哔哩.....等
现在使用微信为模板进行讲解。
微信公众号测试工具
微信测试平台:点击下面链接
登录进去之后呢会出现一个平台,并且会给予测试号专用的appId,和appsecret,一般的正式项目中就是使用自己程序注册的appid,这里进行小测试。
在下方会有一个网页账号,网页授权获取用户基本信息。
点击修改,设置自己的服务器ip和端口号
网页授权获取用户基本信息
就会显示出一个完整的网页授权流程。
跟着目标进行操作即可
第一步:用户同意授权,获取code
将appid改成自己的,以及redirect_uri改成自己设置的服务器地址和端口号
参数说明:
在微信开发工具进行测试:
记得自己微信登录开发工具,并且关注平台测试的公众号,否则不成功。
点击同意后便会获得code
第二步:通过 code 换取网页授权access_token
将测试的appid和appsecret放入链接中
将得到的code放入下面的链接
在网页进行测试,获得token
第三步跳过。刷新token请自己测试
第四步:拉取用户信息(需 scope 为 snsapi_userinfo)
将获得的token 和用户的openId放入下方地址
地址:
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
在postman中进行测试
第五步:验证授权凭证是否有效
http:GET(请使用 https 协议):
https://api.weixin.qq.com/sns/auth?access_token=ACCESS_TOKEN&openid=OPENID
成功则返回ok。
授权码模式到应用程序中
更改Oauth2Config的配置。将appId和appsecret添加到配置中。并设置认证成功后跳转地址(应该是自己的服务器地址,此处测试用了百度)
其中配置中配置了资源的id,当得到用户的token并进行验证后,就可以携带token访问设置的资源服务,且资源服务必须社区对应的资源服务ID
package com.jt.config;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import java.util.UUID;
/**
* Oauth2 是一种认证授权规范,它基于认证和授权定义了一套规则,在这套规则中规定了
* 实现一套认证授权系统需要哪些对象:
* 1)系统资源(数据)
* 2)资源拥有者(用户)
* 3)管理资源的服务器
* 4)对用户进行认证和授权的服务器
* 5)客户端系统(负责提交用户身份信息的系统)
*
* 思考:对于一个认证授权系统来讲,需要什么?:
* 1)提供一个认证的入口?(客户端去哪里认证)
* 2)客户端应该携带什么信息去认证?(username,password,....)
* 3)服务端通过谁去对客户端进行认证(一个负责认证的对象)?
*/
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer //在oauth2规范中启动认证和授权
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenStore tokenStore;
//提供一个认证的入口(客户端去哪里认证)?(http://ip:port/.....)
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//super.configure(security);
//对外发布认证入口(/oauth/token),认证通过服务端会生成一个令牌
security//允许用户通过表单方式提交认证,完成认证
.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
//对外发布检查令牌的入口(/oauth/check_token)
.checkTokenAccess("permitAll()");
super.configure(security);
}
//定义客户端应该携带什么信息去认证?
//指明哪些对象可以到这里进行认证(哪个客户端对象需要什么特点)。
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
//使用内存存储
.inMemory()
//标记客户端id
.withClient("wx6d86e3b762addabc")
//客户端安全码/密钥
.secret(passwordEncoder.encode("2f0aa9d6e67569616c6f159745a82344"))
//设置为true,直接自动授权,成功返回授权码
.autoApprove(true)
//授权后重定向的地址
.redirectUris("http://www.baidu.com")
// 资源的id
.resourceIds("mayikt_resource")
//允许授权的范围
.scopes("all")
//访问令牌的时效
.accessTokenValiditySeconds(30*60)
//刷新令牌的时效
.refreshTokenValiditySeconds(30*60)
//允许授权的类型
.authorizedGrantTypes("authorization_code","password");
}
//提供一个负责认证授权的对象?(完成客户端认证后会颁发令牌,默认令牌格式是uuid方式的)
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//super.configure(endpoints);
//设置认证授权对象
endpoints.authenticationManager(authenticationManager)
//设置令牌业务对象(此对象提供令牌创建及有效机制设置)
.tokenServices(tokenService())//不写,默认是uuid
//设置允许对哪些请求方式进行认证(默认支支持post):可选
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
}
@Bean
public AuthorizationServerTokenServices tokenService(){
//1.构建token业务对象
DefaultTokenServices ts=new DefaultTokenServices();
//2.设置令牌生成机制(创建令牌的方式,存储用户状态信息的方式)
ts.setTokenStore(tokenStore);
//3.设置令牌增强(改变默认令牌创建方式,没有这句话默认是UUID)
ts.setTokenEnhancer(jwtAccessTokenConverter);
//4.设置令牌有效时长(可选)
ts.setAccessTokenValiditySeconds(3600);
//5.设置刷新令牌以及它的有效时时长(可选)
ts.setSupportRefreshToken(true);
ts.setRefreshTokenValiditySeconds(3600*24);
return ts;
}
}
重新配置ResourceConfig:配置认证和Basic登录
package com.jt.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@Configuration
@EnableResourceServer
//启动方法上的权限控制,需要授权才可访问的方法上添加@PreAuthorize等相关注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() //所有请求都需要通过认证
.and()
.httpBasic() //Basic登录
.and()
.csrf().disable(); //关跨域保护
}
}
SecurityConfig配置中只需要两个
package com.jt.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//创建BCryptPasswordEncoder 注入容器 加密方式
@Bean
public BCryptPasswordEncoder passwordEncoder(){
//当你将此对象注入容器时,就会自动将密码进行bc的比对校验。
//如果输入的明文密码与数据库中的加密密码不匹配则报错。
//切数据库中必须存储为bc加密的密码
return new BCryptPasswordEncoder();
}
/**
* 定义认证管理器对象,这个对象负责完成用户信息的认证,
* 即判定用户身份信息的合法性,在基于oauth2协议完成认
* 证时,需要此对象,所以这里讲此对象拿出来交给spring管理
* @return
* @throws Exception
*/
@Bean
@Override //需要通过AuthenticationManager的authenticate方法进行用户认证,所有需要在此将其注入容器
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
配置完成后启动项目
1.在网页进行访问地址:并获得code
http://localhost:8080/oauth/authorize?client_id=mayikt&response_type=code
并将appid填入进去
填完后就会跳转到自己设定的跳转地址上,并且返回code
2.携带code访问地址:获得token
如果没有appid,自己随便写的id则会让你填写账号密码进行访问才可。现在我使用的是微信的测试ip则不会再要账号密码就可以获得token,因为已经是授权的了。
3.携带token进行访问其他服务资源
创建一个新的服务项目,并进行如下配置
1.设置服务的资源
mayikt:
appid: wx6d86e3b762addabc
appsecret: 2f0aa9d6e67569616c6f159745a82344
server:
port: 8081
2.配置ResourceConfig
必须配置资源ID,只有资源ID与上面配置的ID相同才可进行访问。
package com.mayikt.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
/**
* 资源Server端
*/
@Configuration
@EnableResourceServer
public class ResourceConfig extends ResourceServerConfigurerAdapter {
@Value("${mayikt.appid}")
private String mayiktAppId;
@Value("${mayikt.appsecret}")
private String mayiktAppSecret;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Primary
@Bean
public RemoteTokenServices remoteTokenServices() {
final RemoteTokenServices tokenServices = new RemoteTokenServices();
//设置授权服务器check_token端点完整地址 通过该接口进行验证
tokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
//设置客户端id与secret,注意:client_secret值不能使用passwordEncoder加密!
tokenServices.setClientId(mayiktAppId);
tokenServices.setClientSecret(mayiktAppSecret);
return tokenServices;
}
@Override
public void configure(HttpSecurity http) throws Exception {
//设置创建session策略
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
//@formatter:off
//所有请求必须授权
http.authorizeRequests()
.anyRequest().authenticated();
//@formatter:on
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId("mayikt_resource").stateless(true);
}
}
3.创建controller并进行访问
package com.mayikt.service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MemberService {
@GetMapping("/getMember")
public String getMember() {
return "我是会员服务接口";
}
}
不携带token是访问不成功的。不配置资源ID也是不行的
以上就是使用授权码进行的测试。
配置完授权码模式依旧可以使用账号密码的形式:
访问测试
1.获取access_token请求(/oauth/token)
2.检查头肯是否有效请求(/oauth/check_token)
http://localhost:8080/oauth/check_token?token=ea2c1b1e-5541-4018-8728-07f1ac87e9e8
3.刷新token
扩展:
1.很多同学会问,使用授权码模式进行登录后,那些配@PreAuthorize("hasAuthority('sys')")权限的资源 还能访问吗?
答案是不能的,只能是密码模式下登录的,获得权限的才可访问。所以一般授权码都是给用户进行访问的,访问的都是不加权限的功能。而加权限的都是管理员进行使用的那边就需要用账号密码登录