图片如果太小可以右键在新标签打开或者按住 ctrl+鼠标滑轮调整页面尺寸调整。
基础
核心
认证与授权
与Shiro联系
SpringSecurity 在 SpringBoot 出现前因为配置复杂使用较少,但是在SpringBoot 出现后搭配使用开发效率大大提高。是一款重量级框架。而 Shiro 是一款轻量级框架,配置简单一些,所以如果不使用 SpringBoot,那么一般搭配 Shiro,而使用SpringBoot 就搭配 SpringSecurity。
核心接口
UserDetailsService
定义了SpringSecurity 查询用户信息的接口方法,在SpringSecurity 认证时,并不是直接通过用户名密码去数据库比对,没有对应就返回,而是先通过 username 去数据库查到对应的用户信息,然后进行拼接成 SpringSecurity 内部维护的用户对象,然后由内部方法进行密码比对。而查询数据库返回用户对象的接口方法就是由 UserDetailsService 接口定义的。
UserDetails
上面说到数据库查询用户信息会返回一个SpringSecurity 内部维护的用户对象。这个用户抽象类就是 UserDetails,其内部结构如下
public interface UserDetails extends Serializable {
// ~ Methods
// ========================================================================================================
/**
* Returns the authorities granted to the user. Cannot return <code>null</code>.
* 授权列表
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* Returns the password used to authenticate the user.
*
* @return the password
*/
String getPassword();
/**
* Returns the username used to authenticate the user. Cannot return <code>null</code>.
*
* @return the username (never <code>null</code>)
*/
String getUsername();
/**
* Indicates whether the user's account has expired. An expired account cannot be
* authenticated.
* 是否过期
* @return <code>true</code> if the user's account is valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
boolean isAccountNonExpired();
/**
* Indicates whether the user is locked or unlocked. A locked user cannot be
* authenticated.
* 是否锁定,如果锁定就无法验证
* @return <code>true</code> if the user is not locked, <code>false</code> otherwise
*/
boolean isAccountNonLocked();
/**
* Indicates whether the user's credentials (password) has expired. Expired
* credentials prevent authentication.
* 用户凭证是否过期,过期的凭证会阻止身份验证
* @return <code>true</code> if the user's credentials are valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
boolean isCredentialsNonExpired();
/**
* Indicates whether the user is enabled or disabled. A disabled user cannot be
* authenticated.
* 用户是启用还是禁用,无法对禁用的用户进行身份验证
* @return <code>true</code> if the user is enabled, <code>false</code> otherwise
*/
boolean isEnabled();
}
在使用时可以让自定义用户来实现这个接口。
PasswordEncoder
密码接口,一般使用 BCryptPasswordEncoder 来作为默认的密码转换器。SpringSecurity 在加密时引入盐,使得加密过程是不可逆的,而加密后的字符串包含盐信息,在比较方法中会对加密后的密码进行解析,解析出盐值,然后对输入密码进行加密,比较输入密码加密后的结果是否与原密码加密后的结果一致。使用 encode 方法进行加密, matches 方法进行密码比较。如果一致返回 true。
常用配置
用户名密码配置
方式一、配置文件
方式二、配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String encode = passwordEncoder.encode("123");
auth.inMemoryAuthentication().withUser("lucy").password(encode).roles("admin");
// super.configure(auth);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
View Code
方式三、自定义配置
因为一般项目用户名密码都是存在数据库的,所以这是最主流的。
1、配置UserDetails,返回用户信息
@Service("userDetailsService")
public class MyUserDetailService implements UserDetailsService {
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Users user = usersMapper.selectOne(new QueryWrapper<Users>().eq("username", s));
if(user == null){
throw new UsernameNotFoundException("用户名不存在!");
}
// 权限列表,role
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User(s, passwordEncoder.encode(user.getPassword()),auths);
}
}
View Code
2、添加配置类,将userDetails注册进 SpringSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
View Code
记住我
原理
在登陆后会向数据库的 persistent_logins 表中插入一条记录,表结构如下
series 是主键, 随后将 series 和 token 进行算法转换成字符串发给客户端,后面客户端会携带 Cookie ,当下次访问时后端会解析 Cookie ,解析成 series 和 token ,然后去表中匹配,验证token是否一致,以及 last_used + 存活时间是否到期,如果都满足就再以 name 走 UserDetailsService 的方法,返回用户信息。
配置
建表语句:
DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@Resource
private DataSource datasource;
/**
* 注入记住我token表的数据源
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(datasource);
// jdbcTokenRepository.setCreateTableOnStartup(true); // 是否自动创建token数据表,如果是第一次可以勾选,后面表存在还开启就会报错
return jdbcTokenRepository;
}
可以使用在配置方法中添加 ".rememberMeParameter("rem") " 配置记住我功能的name
注意:
1、这里的 last_used 是拒上次打开浏览器登陆开始计算的,也就是每次打开浏览器访问一次 last_used 都会刷新一次。而浏览器内部访问并不会刷新时间。
2、退出后(退出登陆状态)会清除数据库的token数据记录,再次访问需要重新登录
登陆成功处理器
步骤一:增加组件
方式一、继承实现类
@Slf4j
@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws ServletException, IOException {
User user = (User) authentication.getPrincipal(); // 获取Security 内部维护的user对象
System.out.println(user.getUsername()); // 用户名:aa
System.out.println(user.getPassword()); // 密码,由于加密,得到的是null:null
System.out.println(user.getAuthorities()); // 用户权限列表:[ROLE_AAA, ROLE_sale, admins, manager]
response.sendRedirect("http://www.baidu.com");
}
}
View Code
方式二、实现底层接口
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
public MyAuthenticationSuccessHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect(url);
}
}
View Code
步骤二:将组件注册进成功处理器配置中
登陆失败处理器
步骤一:增加组件
方式一、实现接口
方式二、继承实现类
步骤二:将组件注册进失败处理器配置中
权限认证失败处理器
1、组件
2、配置
用户退出处理器
1、组件
2、配置
角色权限
访问一个需要权限或角色的页面需要先登陆,如果登陆后还是不能访问就会返回500.
角色、权限、用户关系
权限与角色是多对多,角色与用户也是多对多。权限指的是对某个表具体的增删改查权限,而角色是一系列权限的集合。比如管理员角色拥有对所有表增删改查的权限,普通用户角色只拥有对所有表查询的权限,而用户 admin 拥有管理员角色,用户 A 拥有普通用户的角色。
定义权限
在config里配置路径所需权限,在UserDetailsService里配置用户所拥有的权限。
1、hasAuthority 是与关系,如果在config里配置了多个权限,如”admin,manager”,那么在UserDetailsService也必须对用户配置两个角色权限才可以访问
2、hasAnyAuthority 是或关系,如果在 config里配置了多个权限,如”admin,manager”,那么在UserDetailsService只需要对用户配置一个权限就可以访问
定义角色
角色在 UserDetailsService 实现类中配置需要加 "ROLE_" 前缀
而hasRole 和hasAnyRole 对应权限里的hasAuthority 和hasAnyAuthority,是与和或的关系。
Access 来定义权限、角色
上面的hasRole、hasAuthority 底层都是使用 access 来实现的,所以我们还可以通过底层的access 方法来主直接定义权限、角色。
那么 config 里配置就是如下:
自定义 Access 校验规则
1、组件
2、配置
基于 IP 来限制
这样的话只能接收来自 127.0.0.1 的请求。
定义角色注解
@Secured单个””里不支持使用,隔开,也就是不支持与关系。如果要配置多个或关系,可以使用{}, 在UserDetailsService里只要配置一个就可以访问。
并且只支持定义角色,不支持定义权限,也就是Secured里必须是ROLE_开头
定义角色、权限注解
可以定义角色、也可以定义权限
如果用户拥有的角色是abc,那么在这里可以配置hasRole(‘abc’),也可以配置hasRole(‘ROLE_abc’),而使用config配置类配置则不可以,会报错。而大小写则和配置类一样会区分
先执行后校验注解
可以用于记录访问日志
对返回和传入数据过滤注解
CSRF
CSRF 是为了防止用户在开启记住登陆后,其他非法用户截取到登陆用户的 Cookie ,登陆其他用户进行非法操作。
默认是开启的,开启后用户登录时,系统发放一个CsrfToken值(key是 _csrf,value是token值),用户携带该CsrfToken值与用户名、密码等参数完成登录。系统记录该会话的 CsrfToken 值,之后在用户的任何请求中,都必须带上该CsrfToken值,并由系统进行校验。
配置:
相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--对Thymeleaf添加Spring Security标签支持-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
开启 CSRF 时配置类不能配置 loginProcessingUrl 和 defaultSuccessUrl 。会影响登陆跳转逻辑。
其他配置
1、如果配置了登陆的URL(也就是loginProcessingUrl),那么自定义Controller里处理的登陆请求就会用不到,走的是SpringSecurity内部的验证方法。
2、anyRequest()必须配置在所有的antMatches后面,也就是笼统的权限配置必须放在其他权限的最后
3、and()是用于连接多个http配置。
4、在开发时需要添加@EnableWebSecurity注解,这个注解会自动配置安全认证策略和认证信息。
整合OAuth2
关于 OAuth2 与 JWT 可以移步 浅谈常见的认证机制 。
基础依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
基础配置
因为 OAuth2 涉及到资源服务器和授权服务器,所以除了配置 SpringSecurity ,还需要配置资源服务器和授权服务器。
1、SpringSecurity配置:
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**", "/login/**", "/logout/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and()
.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
View Code
2、授权服务器配置:定义 app_id、app_secret,以及重定向地址,授权范围等
这里直接贴下包含下面整合 redis 存储、JWT、SSO总的配置,根据图片需要进行选择
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private AuthenticationManager authenticationManager;
@Resource
private UserService userDetailsService;
@Resource
private TokenStore jwtTokenStore; // 使用jwt存储(因为jwt是无状态的,所以并不会持久化)
// @Resource
// private TokenStore redisTokenStore; // 使用redis存储
@Resource
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Resource
private JwtTokenEnhancer jwtTokenEnhancer;
/**
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 设置 JWT 增强内容
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(jwtAccessTokenConverter);
tokenEnhancerChain.setTokenEnhancers(delegates);
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService) // 密码模式需要配置的
.tokenStore(jwtTokenStore)
.tokenEnhancer(tokenEnhancerChain) // 增加额外数据
.accessTokenConverter(jwtAccessTokenConverter); // 使用jwt来代替默认的令牌
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory() // 放入内存
.withClient("client") // 客户端ID
.secret(passwordEncoder.encode("112233")) // 密钥
// 重定向地址,这里整合SSO设置为客户端的login页面是因为SpringSecurity默认登陆页面的URL就是login,在客户端通过授权服务器通过后
// 携带令牌重定向到客户端8081的login页面,自动解析令牌完成登陆。
.redirectUris("http://www.baidu.com")
.scopes("all") // 授权范围
.autoApprove(true) // 开启自动授权(不需要进入授权页面手动选择授权)
.accessTokenValiditySeconds(60) // 过期时间,单位s
.refreshTokenValiditySeconds(86400) // 刷新令牌过期时间
.authorizedGrantTypes("authorization_code","password","refresh_token"); //授权类型:
// authorization_code:授权码模式
// password:密码模式
// refresh_token:支持刷新令牌
}
/**
* 配置单点登陆
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()");
}
}
View Code
3、资源服务器配置:定义资源服务器资源权限角色配置。
4、其他:userDetailsService 配置
@Service("userDetailsService")
public class UserService implements UserDetailsService {
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 权限列表,role
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin1");
return new User(s, passwordEncoder.encode("123456"),auths);
}
}
View Code
自定义用户实体类 user ,权限属性全部设为 true。
public class User implements UserDetails {
private String username;
private String password;
private List<GrantedAuthority> authorities;
public User(String username, String password, List<GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
View Code
资源服务器的资源Controller
同样贴上完整代码,根据图片需要选择
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(HttpServletRequest request,Authentication authentication){
String authorization = request.getHeader("Authorization");
String token = authorization.substring(authorization.lastIndexOf("bearer") + 7);
return Jwts.parser()
.setSigningKey("test_key".getBytes(StandardCharsets.UTF_8)) // 密钥必须和加密所用的一致
.parseClaimsJws(token)
.getBody();
// return authentication.getPrincipal();
}
}
View Code
授权码模式
在上面的授权服务器配置中,已将授权类型设为 授权码模式,所以直接使用上面的配置。
验证
1、获取授权码
访问 http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all ,在登陆成功后(因为走的是上面 userDetailsService 的方法,所以用户名任意,密码123456就通过登陆),会重定向授权服务器配置中配置好的 http://www.baidu.com 。并且携带授权服务器返回的授权码 code。
2、获取授权令牌
接下来就可以再次访问 localhost:8080/oauth/token 携带授权码及其他数据来向授权服务器获取授权令牌。
3、通过令牌访问资源服务器的资源,访问资源服务器上资源的 URL,并携带授权令牌。
密码模式
密码模式因为是通过密码直接获取授权令牌,所以不需要先获取授权码,同时需要设置自定义的 userDetailsService 实现类,以及 authenticationManager 组件
1、ServurityConfig里增加配置:
2、授权服务器增加配置:
这样配置是同时支持授权码模式与密码模式
验证
通过密码获取授权令牌
访问资源服务器的资源则和授权码模式验证一样。
整合 redis 将令牌存入 redis
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2、注册 redis 的 tokenStore 组件进容器
@Configuration
public class RedisConfig {
@Resource
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore redisTokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
}
View Code
3、在授权服务器里注册 tokenStore
4、在配置文件里配置 redis 地址密码等。
使用 JWT 作为令牌
1、增加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2、从容器中移除 redis 的 tokenStore 组件,同时想容器中加入 jwt 的 tokenStore 组件,并且配置 jwt 的转换器
完整代码,根据图片需要自取
@Configuration
public class JwtTokenStoreConfig {
@Resource
private JwtAccessTokenConverter jwtAccessTokenConverter;
// 保存 token的组件
@Bean
public TokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter);
}
// Jwt 转换器,用于将jwt转换成 OAuth2的令牌
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
// 设置jwt密匙
jwtAccessTokenConverter.setSigningKey("test_key");
return jwtAccessTokenConverter;
}
// 配置Jwt的附加信息
@Bean
public JwtTokenEnhancer jwtTokenEnhancer(){
return new JwtTokenEnhancer();
}
}
View Code
3、注册进授权服务器
JWT 增加额外信息
1、增加 Jwt 附加信息组件并注册进容器
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
HashMap<String, Object> map = new HashMap<>();
map.put("enhance", "enhancer info");
((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(map);
return oAuth2AccessToken;
}
}
View Code
2、在授权服务器里配置 jwt 附加信息组件
3、验证,修改资源服务器资源返回的信息
设置过期时间和刷新令牌
在授权服务器里增加配置:
在60s后token令牌(access_token)失效后,可以使用刷新令牌重新获取新的令牌,新的令牌过期时间也是60s。
因为密码模式不支持刷新令牌,所以通过授权码模式使用刷新令牌来获取新的令牌
通过刷新令牌获取令牌
整合SSO(单点登陆)
整合 SSO 后验证的原理就成了下面
各个服务模块都使用同一个授权服务器,也就是图中的认证中心,在第一次访问模块A时会去跳转到授权服务器进行验证,如果通过,那么就会返回给前端一个 token 令牌,以后在访问A或B时,都会携带这个令牌,而验证时都是通过同一个授权服务器验证,所以都会解析通过,进而访问对应的资源。
1、引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2、新建一个模块,配置 SSO 访问授权服务器的地址
server.port=8081
#防止Cookie冲突,冲突会导致登陆验证不通过
server.servlet.session.cookie.name=OAUTH2-CLIENT-SESSIONID1
#授权服务器地址
oauth2-server-url: http://localhost:8080
#与授权服务器对应的配置
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=112233
#获取授权码地址
security.oauth2.client.user-authorization-uri=${oauth2-server-url}/oauth/authorize
#获取令牌地址
security.oauth2.client.access-token-uri=${oauth2-server-url}/oauth/token
#获取jwt令牌地址
security.oauth2.resource.jwt.key-uri=${oauth2-server-url}/oauth/token_key
View Code
3、增加 SSO 模块的资源
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(HttpServletRequest request,Authentication authentication){
return authentication;
}
}
View Code
4、主程序开启 OAuth2 自动配置
5、在授权服务器的配置增加配置
随后访问客户端资源 http://localhost:8081/user/getCurrentUser 就会先跳转到 http://localhost:8080/login ,也就是授权服务器进行授权验证,通过后经重定向回到 http://localhost:8081/login ,也就是客户端的登陆页面,并且携带授权服务器提供的jwt令牌,所以会自动解析通过验证,最后再访问客户端的资源