上一篇文章中基于SpringSecurity已经有了完整的详解,今天这篇文章基于SpringSecurity将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类
- 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;
- }
- }
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>
- 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
- }
- }
- 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);
-
- }
- }
定义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";
- }
- 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;
- }
-
- }
-
-
- 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);
- }
- }
- 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);
- }
- }
- 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自带的登录接口
输入账号密码和自己定义的规则内容就会调用配置访问自己的数据库并进行验证。
定义参数: 全部都是自己定义的

测试得到的token
访问接口:localhost:8080/oauth/check_token
参数:token 将上面得到token 放入
会得到用户的所有信息,包括用户的权限等

测试权限
访问自己设定的接口:

可以看到使用注解配置的权限中与上面得到的用户中的权限数据一致,则访问成功
Headers参数:Authorization 必须将得到的token放于此Headers中。而且必须token前面加上bearer

测试错误token,或者接口权限不对等
当权限不对等时就会调用我们创建的处理器。


上面是通过密码进行登录的模式,现在讲解授权码模式
授权码模式指:通过第三方进行认证授权登录,比如微信,qq,哔哩哔哩.....等
现在使用微信为模板进行讲解。
微信公众号测试工具
微信测试平台:点击下面链接
微信公众平台
https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login&token=1820792496&lang=zh_CN
登录进去之后呢会出现一个平台,并且会给予测试号专用的appId,和appsecret
,一般的正式项目中就是使用自己程序注册的appid,这里进行小测试。

在下方会有一个网页账号,网页授权获取用户基本信息。
点击修改,设置自己的服务器ip和端口号

然后点击网页授权获取用户基本信息
就会显示出一个完整的网页授权流程。

跟着目标进行操作即可
将appid改成自己的,以及redirect_uri改成自己设置的服务器地址和端口号
参数说明:

在微信开发工具进行测试:
记得自己微信登录开发工具,并且关注平台测试的公众号,否则不成功。

点击同意后便会获得code

将测试的appid和appsecret放入链接中
将得到的code放入下面的链接

在网页进行测试,获得token

第三步跳过。刷新token请自己测试
将获得的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,因为已经是授权的了。
创建一个新的服务项目,并进行如下配置
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)
http://localhost:8080/oauth/token?code=IDXeHy&grant_type=authorization_code&redirect_uri=http://www.mayikt.com/callback&scope=all
2.检查头肯是否有效请求(/oauth/check_token)
http://localhost:8080/oauth/check_token?token=ea2c1b1e-5541-4018-8728-07f1ac87e9e8
3.刷新token
http://localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=fbde81ee-f419-42b1-1234-9191f1f95be9&client_id=demoClientId&client_secret=demoClientSecret
扩展:
1.很多同学会问,使用授权码模式进行登录后,那些配@PreAuthorize("hasAuthority('sys')")权限的资源 还能访问吗?
答案是不能的,只能是密码模式下登录的,获得权限的才可访问。所以一般授权码都是给用户进行访问的,访问的都是不加权限的功能。而加权限的都是管理员进行使用的那边就需要用账号密码登录