整体概要
与 shiro
相比,spring security
功能更全面,同样重量级也越大。shiro
相对比较轻量。
spring security
本质就是过滤器链
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
松哥手把手带你捋一遍 Spring Security 登录流程
两个重要接口
UserDetailService
当什么都没有都没配置时,账号和密码都是由spring security
自动生成的。实际中是由我们到数据库中进行查询的,自定义逻辑控制认证逻辑。
UsernamePasswordAuthenticaionFilter
拦截请求路径为/login
的post请求,随后验证用户名与密码。我们需要写一个类继承此类并重写他的三个方法
attemptAuthentication
验证用户名与密码successfulAuthentication
验证成功unsuccessfulAuthentication
验证失败
而UserDetailService
接口 : 查询数据库用户名与密码的过程,返回User对象(此对象时spring security提供的对象)
PasswordEncoder
数据加密接口,用于返回user对象里面的密码加密
用户认证
设置用户名与密码
- 在配置文件中写
- 继承
WebSecurityConfigurerAdapter
,重写configure
方法 - 在
configure
方法中利用UserDetailService
的实现类查询数据库
而服务器识别Session的关键就是依靠一个名为
JSESSIONID
的Cookie。在Servlet中第一次调用req.getSession()
时,Servlet容器自动创建一个Session ID,然后通过一个名为JSESSIONID
的Cookie发送给浏览器:这里要注意的几点是:
JSESSIONID
是由Servlet容器自动创建的,目的是维护一个浏览器会话,它和我们的登录逻辑没有关系;- 登录和登出的业务逻辑是我们自己根据
HttpSession
是否存在一个"user"
的Key判断的,登出后,Session ID并不会改变;- 即使没有登录功能,仍然可以使用
HttpSession
追踪用户,例如,放入一些用户配置信息等。
user类
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = 520L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
spring security对User类进行验证时,首先先检查accountNonExpired;accountNonLocked
等字段,有问题直接throw exception,没问题再查验密码。
初级自定义认证
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService myUserDetailService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService).passwordEncoder(getPasswordEncoder());
}
/**
* 自定义用户认真需要有Bean实现PassWordEncoder接口
* @return
*/
@Bean
protected PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//查询数据库,使用myabtis-plus
QueryWrapper<Users> wrapper=new QueryWrapper<>();
wrapper.eq("username",s);
Users users = userMapper.selectOne(wrapper);
if(users==null) throw new UsernameNotFoundException("用户名不存在");
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
return new User(users.getUsername(),passwordEncoder.encode(users.getPassword()),authorities);
}
}
WebSecurityConfigurerAdapter
configure(HttpSecurity http)
方法
提交给loginProcessingUrl
时,用户名与密码的key默认设置为username
和password
,方法为post
Authentication
我们可以在任何地方注入 Authentication 进而获取到当前登录用户信息,Authentication 本身是一个接口,它有很多实现类。
在这众多的实现类中,我们最常用的就是 UsernamePasswordAuthenticationToken。
username 对应了 UsernamePasswordAuthenticationToken 中的 principal 属性,而 password 则对应了它的 credentials 属性。
User login_user= (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
角色权限
hasAuthority
:该角色是否拥有此权限。当权限不符合时,返回403。下图为写死权限,无法动态
hasAuthority
只有角色拥有其中之一的权限即可
RBAC动态权限设计
用户 —— 角色 : 一对多 ,角色 —— 权限 :一对多
在WebSecurityConfigurerAdapter.configure()
中,将权限从db提取出来并添加进SpringSecurityConfig
中。
通常从Redis中拉去权限,可以将db中表定时写入Redis中
用户登录时,利用关联查询从db得到用户权限,写入User类中(User类需要继承SpringSecutiy的User类)。
oauth2
开放接口平台设计
http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
1、引导用户打开以下网页
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx520c15f417810387&redirect_uri=https%3A%2F%2Fchong.qq.com%2Fphp%2Findex.php%3Fd%3D%26c%3DwxAdapter%26m%3DmobileDeal%26showwxpaytitle%3D1%26vb2ctag%3D4_2030_5_1194_60&response_type=code&scope=snsapi_base&state=123#wechat_redirect
如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE。
回调地址需要与在微信公众号平台登记的一致,否则显示回调地址错误。
2、获取code后,请求以下链接获取access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
正确时返回的JSON数据包如下:
{ "access_token":"ACCESS_TOKEN", "expires_in":7200, "refresh_token":"REFRESH_TOKEN", "openid":"OPENID", "scope":"SCOPE" }
Spring Secutiy Oauth2
Oauth 配置
@Component
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
//允许表单提交
.allowFormAuthenticationForClients()
//开启/oauth/token_key验证端口无权限访问
.checkTokenAccess("permitAll()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//这里写死在程序,实际中从db中捞数据
clients.inMemory()
//client id
.withClient("PS5")
//密码
.secret(passwordEncoder.encode("123456"))
//授权方式
.authorizedGrantTypes("authorization_code")
//权限范围
.scopes("all")
//资源id
.resourceIds("PS5_resource")
//回调地址
.redirectUris("http://localhost:8081");
}
}
启动后,访问以下链接,用户授权登录
http://localhost:8080/oauth/authorize?client_id=PS5&response_type=code
Spring JWT
大部分情况,token其实也存储redis中,并不是完全的无状态的。但是token本身含有用户的一些信息(payload),是通过Base64加密,透明的,所以比较只有一个SessionID更好。
目前企业可以单独出一个服务,专门记录、查询已登录的用户信息。在Redis中无法查询到此登录用户后,再去此服务中查询登录信息。
https://www.jianshu.com/p/6307c89fe3fa