初始权限管理 权限管理概念权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。权限管
理几乎出现在任何系统里面,前提是需要有用户和密码认证的系统。
在权限管理的概念中,有两个非常重要的名词:
认证:通过用户名和密码成功登陆系统后,让系统得到当前用户的角色身份。
授权:系统根据当前用户的角色,给其授予对应可以操作的权限资源。
完成权限管理需要三个对象 用户:主要包含用户名,密码和当前用户的角色信息,可实现认证操作。
角色:主要包含角色名称,角色描述和当前角色拥有的权限信息,可实现授权操作。
权限:权限也可以称为菜单,主要包含当前权限名称,url地址等信息,可实现动态展示菜单。
注:这三个对象中,用户与角色是多对多的关系,角色与权限是多对多的关系,用户与权限没有直接关系,二者是通过角色来建立关联关系的。
初识SpringSecurity Spring Security概念Spring Security是spring采用AOP思想,基于servlet过滤器实现的安全框架。它提供了完善的认证机制和方法级的
授权功能。是一款非常优秀的权限管理框架。
Spring Security简单入门 Spring Security博大精深,设计巧妙,功能繁杂,一言难尽,咱们还是直接上代码吧!
创建web工程并导入jar包Spring Security主要jar包功能介绍
spring-security-core.jar
核心包,任何Spring Security功能都需要此包。
spring-security-web.jar
web工程必备,包含过滤器和相关的Web安全基础结构代码。
spring-security-confifig.jar
用于解析xml配置文件,用到Spring Security的xml配置文件的就要用到此包。
spring-security-taglibs.jar
Spring Security提供的动态标签库,jsp
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-config</artifactId > <version > 5.3.3.RELEASE</version > </dependency > <dependency > <groupId > org.springframework.security</groupId > <artifactId > spring-security-taglibs</artifactId > <version > 5.3.3.RELEASE</version > </dependency >
配置web.xml1 2 3 4 5 6 7 8 9 10 <filter > <filter-name > springSecurityFilterChain</filter-name > <filter-class > org.springframework.web.filter.DelegatingFilterProxy</filter-class > </filter > <filter-mapping > <filter-name > springSecurityFilterChain</filter-name > <url-pattern > /*</url-pattern > </filter-mapping >
配置springSecurity的配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xmlns:aop ="http://www.springframework.org/schema/aop" xmlns:tx ="http://www.springframework.org/schema/tx" xmlns:security ="http://www.springframework.org/schema/security" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd" > <security:http auto-config ="true" use-expressions ="true" > <security:intercept-url pattern ="/**" access ="hasAnyRole('ROLE-USER')" /> </security:http > <security:authentication-manager > <security:authentication-provider > <security:user-service > <security:user name ="user" password ="{noop}user" authorities ="ROLE_USER" /> <security:user name ="admin" password ="{noop}admin" authorities ="ROLE_ADMIN" /> </security:user-service > </security:authentication-provider > </security:authentication-manager > </beans >
启动加载让我们的springSecurity配置文件随着spring的加载而加载
1 <import resource ="classpath:spring-security.xml" />
Spring Security过滤器链 Spring Security常用过滤器介绍过滤器是一种典型的 AOP 思想,关于什么是过滤器,就不赘述了,谁还不知道凡是web 工程都能用过滤器?
接下来咱们就一起看看 Spring Security中这些过滤器都是干啥用的,源码我就不贴出来了,有名字,大家可以自
己在idea中Double Shift去。我也会在后续的学习过程中穿插详细解释。
org.springframework.security.web.context.SecurityContextPersistenceFilter 首当其冲的一个过滤器,作用之重要,自不必多言。
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个
SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续fifilter建立所需的上下文。
SecurityContext中存储了当前用户的认证以及权限信息。
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter 此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager
org.springframework.security.web.header.HeaderWriterFilter 向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
org.springframework.security.web.csrf.CsrfFilter csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,
如果不包含,则报错。起到防止csrf攻击的效果。
org.springframework.security.web.authentication.logout.LogoutFilter北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090 匹配URL为/logout的请求,实现用户退出,清除认证信息。
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter 如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter 由此过滤器可以生产一个默认的退出登录页面
org.springframework.security.web.authentication.www.BasicAuthenticationFilter 此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
org.springframework.security.web.savedrequest.RequestCacheAwareFilter 通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter 针对ServletRequest进行了一次包装,使得request具有更加丰富的API
org.springframework.security.web.authentication.AnonymousAuthenticationFilter 当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
org.springframework.security.web.session.SessionManagementFilter SecurityContextRepository限制同一用户开启多个会话的数量
org.springframework.security.web.access.ExceptionTranslationFilter 异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
org.springframework.security.web.access.intercept.FilterSecurityInterceptor 获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。
好了!这一堆排山倒海的过滤器介绍完了。
那么,是不是spring security一共就这么多过滤器呢?答案是否定的!随着spring-security.xml配置的添加,还
会出现新的过滤器。
那么,是不是spring security每次都会加载这些过滤器呢?答案也是否定的!随着spring-security.xml配置的修
改,有些过滤器可能会被去掉。
登录注销 自定义登录认证界面1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <security:http auto-config ="true" use-expressions ="true" > <security:intercept-url pattern ="/login.jsp" access ="permitAll()" /> <security:intercept-url pattern ="/**" access ="hasAnyRole('ROLE_USER')" /> <security:form-login login-page ="/login.jsp" login-processing-url ="/login" default-target-url ="/index.jsp" authentication-failure-url ="/failer.jsp" /> <security:logout logout-url ="/logout" logout-success-url ="/login.jsp" /> <security:csrf /> </security:http >
修改form表单的跳转路径
此时登入,报403
SpringSecurity 的csrf防护机制CSRF (Cross-site request forgery)跨站请求伪造,是一种难以防范的网络攻击方式。
自己的认证页面,请求方式为POST,但却没有携带token,所以才出现了403权限不足的异常。那么如何处理这个问题呢?
方式一:直接禁用csrf,不推荐。
方式二:在认证页面携带token请求
方式一1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <security:http auto-config ="true" use-expressions ="true" > <security:intercept-url pattern ="/login.jsp" access ="permitAll()" /> <security:intercept-url pattern ="/**" access ="hasAnyRole('ROLE_USER')" /> <security:form-login login-page ="/login.jsp" login-processing-url ="/login" default-target-url ="/index.jsp" authentication-failure-url ="/failer.jsp" /> <security:logout logout-url ="/logout" logout-success-url ="/login.jsp" /> <security:csrf disabled ="true" /> </security:http >
方式二在jsp页面中,引入
1 <%@taglib uri="http://www.springframework.org/security/tags" prefix="security" %>
提交时携带token信息
登出在jsp页面中,引入
1 <%@taglib uri="http://www.springframework.org/security/tags" prefix="security" %>
携带token信息
初步实现认证 让我们自己的UserService接口继承 UserDetailsService1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @Service @Transactional() public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Autowired private RoleService roleService; @Autowired private BCryptPasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername (String s) throws UsernameNotFoundException { try { SysUser user = this .userDao.findByName(s); if (user==null ){ return null ; } List<SimpleGrantedAuthority> authorities=new ArrayList<>(); List<SysRole> roles = user.getRoles(); for (SysRole role : roles) { authorities.add(new SimpleGrantedAuthority(role.getRoleName())); } UserDetails userDetails=new User(user.getUsername(),user.getPassword(),authorities); return userDetails; } catch (Exception e) { e.printStackTrace(); return null ; } } @Override public void save (SysUser user) { user.setPassword(passwordEncoder.encode(user.getPassword())); userDao.save(user); } }
添加加密对象1 2 <bean id ="passwordEncoder" class ="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
指定认证使用的业务对象和加密对象1 2 3 4 5 <security:authentication-manager > <security:authentication-provider user-service-ref ="userServiceImpl" > <security:password-encoder ref ="passwordEncoder" /> </security:authentication-provider > </security:authentication-manager >
注意加密使用的动态加盐加密,所有我们不需要自己去生成盐。而因为加了盐,每次生成的加密密码是不一样的
设置用户状态用户认证业务里,我们封装User对象时,选择了三个构造参数的构造方法,其实还有另一个构造方法:
可以看到,这个构造方法里多了四个布尔类型的构造参数,其实我们使用的三个构造参数的构造方法里这四个布尔
值默认都被赋值为了true,那么这四个布尔值到底是何意思呢?
boolean enabled 是否可用
boolean accountNonExpired 账户是否失效
boolean credentialsNonExpired 密码是否失效
boolean accountNonLocked 账户是否锁定
这四个参数必须同时为true认证才可以,为了节省时间,我只用第一个布尔值做个测试,修改认证业务代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Override public UserDetails loadUserByUsername (String s) throws UsernameNotFoundException { try { SysUser user = this .userDao.findByName(s); if (user==null ){ return null ; } List<SimpleGrantedAuthority> authorities=new ArrayList<>(); List<SysRole> roles = user.getRoles(); for (SysRole role : roles) { authorities.add(new SimpleGrantedAuthority(role.getRoleName())); } UserDetails userDetails=new User(user.getUsername(),user.getPassword(),user.getStatus()==1 ,true ,true ,true ,authorities); return userDetails; } catch (Exception e) { e.printStackTrace(); return null ; } }
此刻,只有用户状态为1的用户才能成功通过认证!
记住我 前台代码
后台1 2 <security:remember-me token-validity-seconds ="60" />
结果形成了一个token
安全性分析记住我功能方便是大家看得见的,但是安全性却令人担忧。因为Cookie毕竟是保存在客户端的,很容易盗取,而且
cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全。那么
这就要提醒喜欢使用此功能的,用完网站要及时手动退出登录,清空认证信息。
此外,SpringSecurity还提供了remember me的另一种相对更安全的实现机制 :在客户端的cookie中,仅保存一个
无意义的加密串(与用户名、密码等敏感数据无关),然后在db中保存该加密串-用户信息的对应关系,自动登录
时,用cookie中的加密串,到db中验证,如果通过,自动登录才算通过。
持久化remember me 信息创建一张表,注意这张表的名称和字段都是固定的,不要修改。
1 2 3 4 5 6 CREATE TABLE `persistent_logins` ( `username` varchar (64 ) NOT NULL , `series` varchar (64 ) NOT NULL , `token` varchar (64 ) NOT NULL , `last_used` timestamp NOT NULL , PRIMARY KEY (`series`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8
然后将spring-security.xml中 改为:
1 2 3 4 5 6 <security:remember-me data-source-ref ="dataSource" token-validity-seconds ="60" remember-me-parameter ="remember-me" />
持久化结果
1 2 3 4 5 6 <security:global-method-security secured-annotations ="enabled" pre-post-annotations ="enabled" jsr250-annotations ="enabled" />
授权注解 开启注解1 2 3 4 5 6 <security:global-method-security secured-annotations ="enabled" pre-post-annotations ="enabled" jsr250-annotations ="enabled" />
注意的是,记得在我们的http上这两个配置
使用三种不同风格的注解
1 2 3 4 5 6 7 @Secured({"ROLE_PRODUCT","ROLE_ADMIN"}) @RolesAllowed("ROLE_PRODUCT,ROLE_ADMIN") @PreAuthorize("hasAnyRole('ROLE_PRODUCT','ROLE_ADMIN')") @RequestMapping("/findAll") public String findAll () { return "product-list" ; }
相关配置1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xmlns:aop ="http://www.springframework.org/schema/aop" xmlns:tx ="http://www.springframework.org/schema/tx" xmlns:security ="http://www.springframework.org/schema/security" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd" > <security:http pattern ="/css/**" security ="none" /> <security:http pattern ="/img/**" security ="none" /> <security:http pattern ="/plugins/**" security ="none" /> <security:http pattern ="/failer.jsp" security ="none" /> <security:http auto-config ="true" use-expressions ="true" > <security:intercept-url pattern ="/login.jsp" access ="permitAll()" /> <security:intercept-url pattern ="/**" access ="hasAnyRole('ROLE_USER')" /> <security:form-login login-page ="/login.jsp" login-processing-url ="/login" default-target-url ="/index.jsp" authentication-failure-url ="/failer.jsp" /> <security:logout logout-url ="/logout" logout-success-url ="/login.jsp" /> <security:csrf disabled ="true" /> <security:remember-me data-source-ref ="dataSource" token-validity-seconds ="60" remember-me-parameter ="remember-me" /> <security:access-denied-handler error-page ="/403.jsp" /> </security:http > <bean id ="passwordEncoder" class ="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" /> <security:authentication-manager > <security:authentication-provider user-service-ref ="userServiceImpl" > <security:password-encoder ref ="passwordEncoder" /> </security:authentication-provider > </security:authentication-manager > <security:global-method-security secured-annotations ="enabled" pre-post-annotations ="enabled" jsr250-annotations ="enabled" /> </beans >
springboot整合 引入依赖1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency >
测试随便写一个接口,访问
用户名是user,密码是随机生成的
配置springsecurity1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package top.codekiller.test.springsecurity_springboot.config;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true,jsr250Enabled = true,prePostEnabled = true) public class springSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private IUserService userService; @Bean public BCryptPasswordEncoder bCryptPasswordEncoder () { return new BCryptPasswordEncoder(); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder()); } @Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login.jsp" ,"/failer.jsp" ,"/css/**" ,"/img/**" ,"/plugin/**" ).permitAll() .antMatchers("/**" ).hasAnyRole("USER" ,"ADMIN" ) .anyRequest() .authenticated() .and() .formLogin() .loginPage("/login.jsp" ) .loginProcessingUrl("/login" ) .defaultSuccessUrl("/index.jsp" ) .failureForwardUrl("/failer.jsp" ) .permitAll() .and() .logout() .logoutUrl("/logout" ) .logoutSuccessUrl("/login.jsp" ) .invalidateHttpSession(true ) .permitAll() .and() .csrf() .disable(); } }
实体类 SysUser1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package top.codekiller.test.springsecurity_springboot.pojo;import com.fasterxml.jackson.annotation.JsonIgnore;import lombok.Data;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;import java.util.List;@Data public class SysUser implements UserDetails { private Integer id; private String username; private String password; private Integer status; private List<SysRole> roles; @JsonIgnore @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles; } @JsonIgnore @Override public boolean isAccountNonExpired () { return true ; } @JsonIgnore @Override public boolean isAccountNonLocked () { return true ; } @JsonIgnore @Override public boolean isCredentialsNonExpired () { return true ; } @JsonIgnore @Override public boolean isEnabled () { return true ; } }
SysRole1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package top.codekiller.test.springsecurity_springboot.pojo;import com.fasterxml.jackson.annotation.JsonIgnore;import lombok.Data;import org.springframework.security.core.GrantedAuthority;@Data public class SysRole implements GrantedAuthority { private Integer id; private String roleName; private String roleDesc; @JsonIgnore @Override public String getAuthority () { return this .roleName; } }
mapper接口 UserMapper1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package top.codekiller.test.springsecurity_springboot.mapper;import tk.mybatis.mapper.common.Mapper;import org.apache.ibatis.annotations.*;import top.codekiller.test.springsecurity_springboot.pojo.SysUser;import java.util.List;public interface UserMapper extends Mapper <SysUser > { @Select("select * from sys_user where username=#{name}") @Results({ @Result(id=true,property = "id",column = "id"), @Result(property = "roles",column = "id",javaType = List.class, many = @Many(select = "top.codekiller.test.springsecurity_springboot.mapper.RoleMapper.findByUid")) }) SysUser findByName (String name) ; }
RoleMapper1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package top.codekiller.test.springsecurity_springboot.mapper;import tk.mybatis.mapper.common.Mapper;import org.apache.ibatis.annotations.Select;import top.codekiller.test.springsecurity_springboot.pojo.SysRole;import java.util.List;public interface RoleMapper extends Mapper <SysRole > { @Select("select r.id,r.role,r.role_name roleName,r.role_desc roleDesc from sys_role r,sys_user_role ur where r.id=ur.rid and ur.uid=#{uid} ") List<SysRole> findByUid (Integer uid) ; }
service1 2 3 4 5 6 7 public interface IUserService extends UserDetailsService {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Service @Transactional(rollbackFor = Exception.class) public class UserServiceImpl implements IUserService { private UserMapper userMapper; @Override public UserDetails loadUserByUsername (String s) throws UsernameNotFoundException { return this .userMapper.findByName(s); } }
controller1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package top.codekiller.test.springsecurity_springboot.controller;import org.springframework.security.access.annotation.Secured;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controller @RequestMapping("/product") public class ProductController { @Secured("ROLE_PRODUCT") @RequestMapping("/findAll") @ResponseBody public String findAll () { return "product-list" ; } }
springboot结合JWT的使用 JWT基本概念JWT 生成的token由三部分组成:
头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。
载荷:token中存放有效信息的部分,比如用户名,用户角色,过期时间等,但是不要放密码,会泄露!
签名:将头部与载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。
Rsa基本概念基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
私钥加密,持有私钥或公钥才可以解密
公钥加密,持有私钥才可解密
优点:安全,难以破解
缺点:算法比较耗时,为了安全,可以接受
历史:三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三
个人的名字缩写:RSA。
认证服务 引入依赖列出主要安全依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.9.0</version > </dependency > <dependency > <groupId > joda-time</groupId > <artifactId > joda-time</artifactId > <version > 2.10.5</version > </dependency >
配置application.yml1 2 3 4 5 6 7 manager: jwt: secret: ea61b46dse2@manager@9ds966@codekiller@33da pubKeyPath: E:\chrome\token\\rsa.pub priKeyPath: E:\chrome\token\\rsa.pri expire: 30 headerName: Authorization
配置properties1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package top.codekiller.test.springsecurity.properties;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;import top.codekiller.test.springsecurity.utils.RsaUtils;import javax.annotation.PostConstruct;import java.io.File;import java.security.PrivateKey;import java.security.PublicKey;@ConfigurationProperties(prefix = "manager.jwt") @Slf4j @Data public class JwtProperties { private String secret; private String pubKeyPath; private String priKeyPath; private int expire; private PublicKey publicKey; private PrivateKey privateKey; private String headerName; @PostConstruct public void init () { try { File pubKey = new File(pubKeyPath); File priKey = new File(priKeyPath); if (!pubKey.exists() || !priKey.exists()) { RsaUtils.generateKey(pubKeyPath, priKeyPath, secret); } this .publicKey = RsaUtils.getPublicKey(pubKeyPath); this .privateKey = RsaUtils.getPrivateKey(priKeyPath); } catch (Exception e) { log.error("初始化公钥和私钥失败!" , e); throw new RuntimeException(); } } }
实体类 SysUser (UserDetails)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 @Data public class SysUser implements UserDetails { private Integer id; private String username; private String password; private Integer status; private List<SysRole> roles; @JsonIgnore @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles; } @JsonIgnore @Override public boolean isAccountNonExpired () { return true ; } @JsonIgnore @Override public boolean isAccountNonLocked () { return true ; } @JsonIgnore @Override public boolean isCredentialsNonExpired () { return true ; } @JsonIgnore @Override public boolean isEnabled () { return true ; } }
SysRole(GrantedAuthority)1 2 3 4 5 6 7 8 9 10 11 12 13 @Data public class SysRole implements GrantedAuthority { private Integer id; private String roleName; private String roleDesc; @JsonIgnore @Override public String getAuthority () { return this .roleName; } }
mapper RoleMapper1 2 3 4 5 public interface RoleMapper { @Select("select r.id,r.role_name,r.role_desc from sys_role r,sys_user_role ur where r.id=ur.rid and ur.uid=#{uid} ") List<SysRole> findByUid (Integer uid) ; }
UserMapper1 2 3 4 5 6 7 8 9 10 11 public interface UserMapper { @Select("select * from sys_user where username=#{name}") @Results({ @Result(id=true,property = "id",column = "id"), @Result(property = "roles",column = "id",javaType = List.class, many = @Many(select = "top.codekiller.test.springsecurity.mapper.RoleMapper.findByUid")) }) SysUser findByName (String name) ; }
UserService登录的验证
1 2 public interface IUserService extends UserDetailsService {}
1 2 3 4 5 6 7 8 9 10 11 12 @Service @Transactional(rollbackFor = Exception.class) public class UserServiceImpl implements IUserService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername (String s) throws UsernameNotFoundException { return this .userMapper.findByName(s); } }
WebSecurityConfigsecurity的配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @EnableConfigurationProperties(JwtProperties.class) @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private IUserService userService; @Autowired private JwtProperties jwtProperties; @Bean public BCryptPasswordEncoder bCryptPasswordEncoder () { return new BCryptPasswordEncoder(); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder()); } @Override protected void configure (HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .authorizeRequests() .antMatchers("/**" ).hasAnyRole("USER" ,"ADMIN" ) .anyRequest() .authenticated() .and() .logout() .logoutUrl("/logout" ) .invalidateHttpSession(true ) .permitAll() .and() .addFilter(new JwtAccreditFilter(super .authenticationManager(),this .jwtProperties)) .addFilter(new JwtVerifyFilter(super .authenticationManager(),this .jwtProperties)) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER); } }
编写过滤器 JwtAccreditFilter1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 @Slf4j public class JwtAccreditFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; private JwtProperties jwtProperties; private ObjectMapper objectMapper=new ObjectMapper(); public JwtAccreditFilter (AuthenticationManager authenticationManager, JwtProperties jwtProperties) { this .authenticationManager = authenticationManager; this .jwtProperties = jwtProperties; } @Override public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { SysUser sysUser = objectMapper.readValue(request.getInputStream(), SysUser.class); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword()); return this .authenticationManager.authenticate(authRequest); } catch (IOException e) { PrintWriter writer=null ; try { response.setContentType("application/json;charset=utf-8" ); response.setStatus(HttpStatus.UNAUTHORIZED.value()); writer= response.getWriter(); Map<String,Object> map=new HashMap<>(16 ); map.put("result_code" ,HttpStatus.UNAUTHORIZED.value()); map.put("result_reason" ,"用户名或者密码错误" ); writer.write(JsonUtils.serialize(map)); writer.flush(); } catch (IOException ex) { log.error("登录出错" ,e); }finally { if (writer!=null ){ writer.close(); } } throw new RuntimeException(e); } } @Override protected void successfulAuthentication (HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SysUser user=new SysUser(); user.setUsername(authResult.getName()); user.setRoles((List<SysRole>)authResult.getAuthorities()); try { String token = JwtUtils.generateTokenExpireInMinutes(user, this .jwtProperties.getPrivateKey(), this .jwtProperties.getExpire()*60 ); response.addHeader(this .jwtProperties.getHeaderName(), "Bearer " + token); } catch (Exception e) { PrintWriter writer=null ; try { response.setContentType("application/json;charset=utf-8" ); response.setStatus(HttpStatus.OK.value()); writer= response.getWriter(); Map<String,Object> map=new HashMap<>(16 ); map.put("result_code" ,HttpStatus.OK.value()); map.put("result_reason" ,"认证通过" ); writer.write(JsonUtils.serialize(map)); writer.flush(); } catch (IOException ex) { log.error("授权失败" ,e); }finally { if (writer!=null ){ writer.close(); } } throw new RuntimeException(e); } } }
JwtVerifyFilter1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 @Slf4j public class JwtVerifyFilter extends BasicAuthenticationFilter { private JwtProperties jwtProperties; public JwtVerifyFilter (AuthenticationManager authenticationManager, JwtProperties jwtProperties) { super (authenticationManager); this .jwtProperties = jwtProperties; } @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header=request.getHeader("Authorization" ); if (StringUtils.isBlank(header)){ chain.doFilter(request,response); this .responseJson(response); }else { String token = header.replace("Bearer " , "" ); System.out.println("token" +token); try { Payload<SysUser> payload = JwtUtils.getInfoFromToken(token, this .jwtProperties.getPublicKey(),SysUser.class); SysUser user=payload.getUserInfo(); if (user!=null ){ UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(user.getUsername(),null ,user.getRoles()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); chain.doFilter(request,response); } } catch (Exception e) { log.error("认证出错" ,e); } } } private void responseJson (HttpServletResponse response) { PrintWriter writer=null ; try { response.setContentType("application/json;charset=utf-8" ); response.setStatus(HttpStatus.FORBIDDEN.value()); writer= response.getWriter(); Map<String,Object> map=new HashMap<>(16 ); map.put("result_code" ,HttpStatus.FORBIDDEN.value()); map.put("result_reason" ,"请登录!" ); writer.write(JsonUtils.serialize(map)); writer.flush(); } catch (IOException ex) { log.error("认证发送json数据IO错误" ,ex); }finally { if (writer != null ) { writer.close(); } } } }
资源服务 实体类和认证服务的一样,User和Role连个必要实体类
点击
WebSecurityConfig1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @EnableConfigurationProperties(JwtProperties.class) @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtProperties jwtProperties; @Override protected void configure (HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .authorizeRequests() .antMatchers("/product" ).hasAnyRole("ADMIN" ) .anyRequest() .authenticated() .and() .addFilter(new JwtVerifyFilter(super .authenticationManager(),this .jwtProperties)) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER); } }
配置application.yml1 2 3 4 manager: jwt: pubKeyPath: E:\chrome\token\\rsa.pub # 公钥地址 headerName: Authorization
配置properties1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @ConfigurationProperties(prefix = "manager.jwt") @Slf4j @Data public class JwtProperties { private PublicKey publicKey; private String pubKeyPath; private String headerName; @PostConstruct public void init () { try { this .publicKey = RsaUtils.getPublicKey(pubKeyPath); } catch (Exception e) { log.error("初始化公钥失败!" , e); throw new RuntimeException(); } } }
认证过滤器(JwtVerifyFilter)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 @Slf4j public class JwtVerifyFilter extends BasicAuthenticationFilter { private JwtProperties jwtProperties; public JwtVerifyFilter (AuthenticationManager authenticationManager, JwtProperties jwtProperties) { super (authenticationManager); this .jwtProperties = jwtProperties; } @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header=request.getHeader(this .jwtProperties.getHeaderName()); if (StringUtils.isBlank(header)){ chain.doFilter(request,response); this .responseJson(response); }else { String token = header.replace("Bearer " , "" ); System.out.println("token" +token); try { Payload<SysUser> payload = JwtUtils.getInfoFromToken(token, this .jwtProperties.getPublicKey(),SysUser.class); SysUser user=payload.getUserInfo(); if (user!=null ){ UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(user.getUsername(),null ,user.getRoles()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); chain.doFilter(request,response); } } catch (Exception e) { log.error("认证出错" ,e); } } } private void responseJson (HttpServletResponse response) { PrintWriter writer=null ; try { response.setContentType("application/json;charset=utf-8" ); response.setStatus(HttpStatus.FORBIDDEN.value()); writer= response.getWriter(); Map<String,Object> map=new HashMap<>(16 ); map.put("result_code" ,HttpStatus.FORBIDDEN.value()); map.put("result_reason" ,"请登录!" ); writer.write(JsonUtils.serialize(map)); writer.flush(); } catch (IOException ex) { log.error("认证发送json数据IO错误" ,ex); }finally { if (writer != null ) { writer.close(); } } } }
结果写一个接口进行测试
1 2 3 4 5 6 7 8 9 10 @RestController @RequestMapping("/product") public class ProductController { @Secured({"ROLE_ADMIN"}) @RequestMapping("/findAll") public String findAll () { return "product-list" ; } }
进行登录,获取token
访问资源服务
OAuth2 概念说明 先说OAuth,OAuth是Open Authorization的简写。
OAuth协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是
OAuth的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与
密码就可以申请获得该用户资源的授权,因此OAuth是安全的。
OAuth2.0是OAuth协议的延续版本,但不向前兼容(即完全废止了OAuth1.0)。
使用场景 假设,A网站是一个打印照片的网站,B网站是一个存储照片的网站,二者原本毫无关联。
如果一个用户想使用A网站打印自己存储在B网站的照片,那么A网站就需要使用B网站的照片资源才行。
按照传统的思考模式,我们需要A网站具有登录B网站的用户名和密码才行,但是,现在有了OAuth2,只需要A网
站获取到使用B网站照片资源的一个通行令牌即可!这个令牌无需具备操作B网站所有资源的权限,也无需永久有
效,只要满足A网站打印照片需求即可。
这么听来,是不是有点像单点登录?NONONO!千万不要混淆概念!单点登录是用户一次登录,自己可以操作其
他关联的服务资源。OAuth2则是用户给一个系统授权,可以直接操作其他系统资源的一种方式。
但SpringSecurity的OAuth2也是可以实现单点登录的!
总结一句:SpringSecurity的OAuth2可以做服务之间资源共享,也可以实现单点登录!
OAuth2.0 中四种授权方式 **授权码模式(**authorization code)流程
用户访问客户端,客户端通过用户代理向认证服务器请求授权码;(授权码只能使用一次) 用户同意授权; 认证服务器通过用户代理返回授权码给客户端; 客户端携带授权码向认证服务器请求访问令牌(AccessToken); 认证服务器返回访问令牌; 客户端携带访问令牌向资源服务器请求资源; 资源服务器返回资源。
使用场景
授权码模式是OAuth2中最安全最完善的一种模式,应用场景最广泛,可以实现服务之间的调用,常见的微
信,QQ等第三方登录也可采用这种方式实现。
简化模式(implicit)流程
说明:简化模式中没有【A服务认证服务】这一部分,全部有【A服务客户端】与B服务交互,整个过程不再有
授权码,token直接暴露在浏览器。
用户访问客户端,客户端通过用户代理向认证服务器请求授权码; 用户同意授权; 认证服务器返回一个重定向地址,该地址的url的Hash部分包含了令牌; 用户代理向资源服务器发送请求,其中不带令牌信息; 资源服务器返回一个网页,其中包含的脚本可以获取Hash中的令牌; 用户代理执行脚本提取令牌; 用户代理将令牌返回给客户端; 客户端携带令牌向资源服务器请求资源; 资源服务器返回资源。
使用场景
适用于A服务没有服务器的情况。比如:纯手机小程序,JavaScript语言实现的网页插件等。
密码模式(resource owner password credentials)流程
用户向客户端提供用户名密码; 客户端将用户名和密码发给认证服务器请求令牌; 认证服务器确认无误后,向客户端提供访问令牌; 客户端携带令牌向资源服务器请求访问资源; 资源服务器返回资源。
使用场景
此种模式虽然简单,但是用户将B服务的用户名和密码暴露给了A服务,需要两个服务信任度非常高才能使
用。
**客户端模式(**client credentials)流程
说明:这种模式其实已经不太属于OAuth2的范畴了。A服务完全脱离用户,以自己的身份去向B服务索取
token。换言之,用户无需具备B服务的使用权也可以。完全是A服务与B服务内部的交互,与用户无关了。
客户端向认证服务器进行身份认证,并要求一个访问令牌; 认证服务器确认无误后,向客户端提供访问令牌; 客户端携带令牌向资源服务器请求访问资源; 资源服务器返回资源。
使用场景
A服务本身需要B服务资源,与用户无关。
建表官网地址
官网
核心表:oauth_client_details
字段名 字段说明 client_id 主键,必须唯一,不能为空. 用于唯一标识每一个客户端(client); 在注册时必须填写(也可由服务 端自动生成). 对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appKey,与client_id是同一个概念. resource_ids 客户端所能访问的资源id集合,多个资源时用逗号(,)分隔,如: “unity-resource,mobile-resource”. 该字段的值必须来源于与security.xml
中标签‹oauth2:resource-server
的属性resource-id
值一致. 在security.xml
配置有几个‹oauth2:resource-server
标签, 则该字段可以使用几个该值. 在实际应用中, 我们一般将资源进行分类,并分别配置对应的‹oauth2:resource-server
,如订单资源配置一个‹oauth2:resource-server
, 用户资源又配置一个‹oauth2:resource-server
. 当注册客户端时,根据实际需要可选择资源id,也可根据不同的注册流程,赋予对应的资源id. client_secret appKey,与client_id是同一个概念. 用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成). 对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appSecret,与client_secret是同一个概念. scope 指定客户端申请的权限范围,可选值包括read ,write ,trust ;若有多个权限范围用逗号(,)分隔,如: “read,write”. scope的值与security.xml
中配置的‹intercept-url
的access
属性有关系. 如‹intercept-url
的配置为 ‹intercept-url pattern="/m/**" access="ROLE_MOBILE,SCOPE_READ"/>
则说明访问该URL时的客户端必须有read 权限范围. write 的配置值为SCOPE_WRITE , trust 的配置值为SCOPE_TRUST . 在实际应该中, 该值一般由服务端指定, 常用的值为read,write . authorized_grant_types 指定客户端支持的grant_type,可选值包括authorization_code ,password ,refresh_token ,implicit ,client_credentials , 若支持多个grant_type用逗号(,)分隔,如: “authorization_code,password”. 在实际应用中,当注册时,该字段是一般由服务器端指定的,而不是由申请者去选择的,最常用的grant_type组合有: “authorization_code,refresh_token”(针对通过浏览器访问的客户端); “password,refresh_token”(针对移动设备的客户端). implicit 与client_credentials 在实际中很少使用. web_server_redirect_uri 客户端的重定向URI,可为空, 当grant_type为authorization_code
或implicit
时, 在Oauth的流程中会使用并检查与注册时填写的redirect_uri是否一致. 下面分别说明: 当grant_type=authorization_code
时, 第一步 从 spring-oauth-server获取 "code"
时客户端发起请求时必须有redirect_uri
参数, 该参数的值必须与 web_server_redirect_uri
的值一致. 第二步 用 "code" 换取 "access_token"
时客户也必须传递相同的redirect_uri
. 在实际应用中, web_server_redirect_uri 在注册时是必须填写的, 一般用来处理服务器返回的code
, 验证state
是否合法与通过code
去换取access_token
值. 在spring-oauth-client 项目中, 可具体参考AuthorizationCodeController.java
中的authorizationCodeCallback
方法. 当grant_type=implicit
时通过redirect_uri
的hash值来传递access_token
值.如: http://localhost:7777/spring-oauth-client/implicit#access_token=dc891f4a-ac88-4ba6-8224-a2497e013865&token_type=bearer&expires_in=43199
然后客户端通过JS等从hash值中取到access_token
值. authorities 指定客户端所拥有的Spring Security的权限值,可选, 若有多个权限值,用逗号(,)分隔, 如: “ROLE_UNITY,ROLE_USER”. 对于是否要设置该字段的值,要根据不同的grant_type来判断, 若客户端在Oauth流程中需要用户的用户名(username)与密码(password)的(authorization_code
,password
), 则该字段可以不需要设置值,因为服务端将根据用户在服务端所拥有的权限来判断是否有权限访问对应的API. 但如果客户端在Oauth流程中不需要用户信息的(implicit
,client_credentials
), 则该字段必须要设置对应的权限值, 因为服务端将根据该字段值的权限来判断是否有权限访问对应的API. (请在spring-oauth-client 项目中来测试不同grant_type时authorities的变化) access_token_validity 设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时). 在服务端获取的access_token JSON数据中的expires_in
字段的值即为当前access_token的有效时间值. 在项目中, 可具体参考DefaultTokenServices.java
中属性accessTokenValiditySeconds
. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义. refresh_token_validity 设定客户端的refresh_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天). 若客户端的grant_type不包括refresh_token
,则不用关心该字段 在项目中, 可具体参考DefaultTokenServices.java
中属性refreshTokenValiditySeconds
. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端自定义. additional_information 这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的数据,如: {"country":"CN","country_code":"086"}
按照spring-security-oauth
项目中对该字段的描述 Additional information for this client, not need by the vanilla OAuth protocol but might be useful, for example,for storing descriptive information. (详见ClientDetails.java
的getAdditionalInformation()
方法的注释)在实际应用中, 可以用该字段来存储关于客户端的一些其他信息,如客户端的国家,地区,注册时的IP地址等等. create_time 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) archived 用于标识客户端是否已存档(即实现逻辑删除),默认值为"0"(即未存档). 对该字段的具体使用请参考CustomJdbcClientDetailsService.java
,在该类中,扩展了在查询client_details的SQL加上archived = 0 条件 (扩展字段) trusted 设置客户端是否为受信任的,默认为"0"(即不受信任的,1为受信任的). 该字段只适用于grant_type="authorization_code"的情况,当用户登录成功后,若该值为0,则会跳转到让用户Approve的页面让用户同意授权, 若该字段为1,则在登录后不需要再让用户Approve同意授权(因为是受信任的). 对该字段的具体使用请参考OauthUserApprovalHandler.java
. (扩展字段) autoapprove 设置用户是否自动Approval操作, 默认值为 “false”, 可选值包括 “true”,“false”, “read”,“write”. 该字段只适用于grant_type="authorization_code"的情况,当用户登录成功后,若该值为"true"或支持的scope值,则会跳过用户Approve的页面, 直接授权. 该字段与 trusted 有类似的功能, 是 spring-security-oauth2 的 2.0 版本后添加的新属性.
导包1 2 3 4 5 6 7 8 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-oauth2</artifactId > </dependency >
授权中心的安全配置 配置文件1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 server: port: 8082 spring: datasource: username: root password: root url: jdbc:mysql://localhost:3306/spring_security?characterEncoding=UTF-8&serverTimezone=UTC driver-class-name: com.mysql.jdbc.Driver main: allow-bean-definition-overriding: true mybatis: type-aliases-package: top.codekiller.security.pojo configuration: map-underscore-to-camel-case: true logging: level: top.codekiller.security: debug
实体类1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Data public class SysUser implements UserDetails { private Integer id; private String username; private String password; private Integer status; private List<SysRole> roles; @JsonIgnore @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles; } @JsonIgnore @Override public boolean isAccountNonExpired () { return true ; } @JsonIgnore @Override public boolean isAccountNonLocked () { return true ; } @JsonIgnore @Override public boolean isCredentialsNonExpired () { return true ; } @JsonIgnore @Override public boolean isEnabled () { return true ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data public class SysRole implements GrantedAuthority { private Integer id; private String roleName; private String roleDesc; @JsonIgnore @Override public String getAuthority () { return this .roleName; } }
service1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public interface IUserService extends UserDetailsService {} @Service @Transactional(rollbackFor = Exception.class) public class UserServiceImpl implements IUserService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername (String s) throws UsernameNotFoundException { return this .userMapper.findByName(s); } }
mapper1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface UserMapper { @Select("select * from sys_user where username=#{name}") @Results({ @Result(id=true,property = "id",column = "id"), @Result(property = "roles",column = "id",javaType = List.class, many = @Many(select = "top.codekiller.security.mapper.RoleMapper.findByUid")) }) SysUser findByName (String name) ; } public interface RoleMapper { @Select("select r.id,r.role_name,r.role_desc from sys_role r,sys_user_role ur where r.id=ur.rid and ur.uid=#{uid} ") List<SysRole> findByUid (Integer uid) ; }
security配置类1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 package top.codekiller.security.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.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import top.codekiller.security.service.IUserService;@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private IUserService userService; @Bean public BCryptPasswordEncoder bCryptPasswordEncoder () { return new BCryptPasswordEncoder(); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder()); } @Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginProcessingUrl("/login" ) .permitAll() .and() .csrf() .disable(); } @Override @Bean public AuthenticationManager authenticationManagerBean () throws Exception { return super .authenticationManagerBean(); } }
oauth配置类1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 package top.codekiller.security.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.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.approval.ApprovalStore;import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;import org.springframework.security.oauth2.provider.token.TokenStore;import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;import top.codekiller.security.service.IUserService;import javax.sql.DataSource;@Configuration @EnableAuthorizationServer public class OAuthServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private DataSource dataSource; @Autowired private IUserService userService; @Autowired private AuthenticationManager authenticationManager; @Bean public JdbcClientDetailsService jdbcClientDetailsService () { return new JdbcClientDetailsService(dataSource); } @Bean public TokenStore tokenStore () { return new JdbcTokenStore(dataSource); } @Bean public ApprovalStore approvalStore () { return new JdbcApprovalStore(dataSource); } @Bean public AuthorizationCodeServices authorizationCodeServices () { return new JdbcAuthorizationCodeServices(dataSource); } @Override public void configure (ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(jdbcClientDetailsService()); } @Override public void configure (AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients(); security.checkTokenAccess("isAuthenticated()" ); } @Override public void configure (AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager) .approvalStore(approvalStore()) .authorizationCodeServices(authorizationCodeServices()) .tokenStore(tokenStore()) .userDetailsService(userService); } }
资源服务的安全配置 配置文件1 2 3 4 5 6 7 8 9 10 11 12 13 14 server: port: 8081 spring: datasource: username: root password: root url: jdbc:mysql://localhost:3306/spring_security?characterEncoding=UTF-8&serverTimezone=UTC driver-class-name: com.mysql.jdbc.Driver main: allow-bean-definition-overriding: true logging: level: top.codekiller.security: debug
配置类tokenstore的常用策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package top.codekiller.security.config;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.http.HttpMethod;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;import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;import org.springframework.security.oauth2.provider.token.TokenStore;import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;import javax.sql.DataSource;@Configuration @EnableResourceServer public class OAuthConfig extends ResourceServerConfigurerAdapter { @Autowired private DataSource dataSource; public TokenStore jdbcTokenStore () { return new JdbcTokenStore(dataSource); } @Override public void configure (ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("product_api" ).tokenStore(jdbcTokenStore()); } @Override public void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(HttpMethod.GET, "/**" ) .access("#oauth2.hasScope('read')" ) .antMatchers(HttpMethod.POST, "/**" ).access("#oauth2.hasScope('write')" ) .antMatchers(HttpMethod.PATCH, "/**" ).access("#oauth2.hasScope('write')" ) .antMatchers(HttpMethod.PUT, "/**" ).access("#oauth2.hasScope('write')" ) .antMatchers(HttpMethod.DELETE, "/**" ).access("#oauth2.hasScope('write')" ) .and() .headers().addHeaderWriter((request, response) -> { response.addHeader("Access-Control-Allow-Origin" , "*" ); if (request.getMethod().equals("OPTIONS" )) { response.setHeader("Access-Control-Allow-Methods" , request.getHeader("Access- Control-Request-Method" )); response.setHeader("Access-Control-Allow-Headers" , request.getHeader("Access- Control-Request-Headers" )); } }); } }
测试接口1 2 3 4 5 6 7 8 9 10 @RestController @RequestMapping("/product") public class ProductController { @Secured("ROLE_ADMIN") @GetMapping("/findAll") public String findAll () { return "参评列表查询成功" ; } }
授权码模式测试访问http://localhost:8082/oauth/authorize?response_type=code&client_id=test_one
跳转到登录处理界面
因为我们在配置中配置了login地址
填写账号密码成功后选择权限
获取授权码
跳转地址是在数据库记录的地址
获取token
grant_type是授权码模式,共五种选项client_credentials 客户端模式 implicit 简单模式 authorization_code 授权码模式 refresh_token 刷新token password 密码模式
携带token进行访问资源
注意:授权码只能使用一次
简单模式不建议使用,token直接出现在地址栏,不安全!
访问 http://localhost:8082/oauth/authorize?response_type=token&client_id=test_one
可以看到,我们获取的token是一样的,因为当前用户的token没有过期
密码模式
使用该token进行访问
可以看到,我们获取的token是一样的,因为当前用户的token没有过期
客户端模式此时不存在刷新token
刷新token客户端模式没有刷新token
我这里报了一个错误
1 Handling error: IllegalStateException, UserDetailsService is required.
解决方法:
https://blog.csdn.net/qq_44766883/article/details/106651024