安全性框架
Apache Shiro
比较简单易用,不依赖Spring
应用场景: 传统的SSM项目
Spring Security
比较复杂,功能较强大,属于Spring框架技术
应用场景: Springboot + Springcloud
JWT + SpringSecurity组合,多用于微服务分布式开发中.
JWT + SpringSecurity + SpringCloud
JWT + SpringSecurity + SpringCloud + 前端(AngularJS,VueJS)
了解SpringSecurity核心组件
SecurityContext
SpringSecurity的上下文,保存重要对象的信息,比如,用户信息
SecurityContextHolder
通过该工具获取SecurityContext
Authentication
"认证"的意思,理解成认证的主体,获取认证的信息,账号和密码
接口
表示"用户的详情信息",规范了用户的详情信息
接口
仅定义了一个方法: loadUserByUsername(String username)
入门测试
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>SysUser
@Data
public class SysUser implements UserDetails {
private String id;
private String username;
private String password;
private String locked;
@Override //返回用户的权限信息
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override //判断账号是否过期
public boolean isAccountNonExpired() {
return true;
}
@Override //判断账号是否被锁定
public boolean isAccountNonLocked() {
return this.locked.equals("0");
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override //判断用户是否被禁用
public boolean isEnableed() {
return true;
}
}SysUserService
@Service
public class SysUserService implements UserDetailsService {
@Autowired
private SysUserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("用户名:" + username); //日志输出
SysUserExample example = new SysUserExample();
SysUserExample.Criteria criteria = example.createCriteria();
criteria.andUsercodeEqualTo(username);
List<SysUser> list = userMapper.selectByExample(example);
if (list.size() == 0) {
throw new UsernameNotFoundException("账号不存在");
}
return list.get(0);
}
}SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigApapter {
@Autowired
private SysUserService userService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); //指定加密算法,在springsecurity中密码必须要加密
}
@Override
protected void configure(AuthenticationManagerBuilder anth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/doLogin")
//.loginPage("/login") //指定登录页,不设置框架提供测试页
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
//获取用户对象的信息
SysUser user = (SysUser)authentication.getPrincipal();
ResponseData<SysUser> ret = new ResponseData(200, "登陆成功", user);
//java对象 --> json字符串
String json = new ObjectMapper().writeValueAsString(ret);
System.out.pringln(json);
out.write(json);
out.flush();
out.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
}
})
.permitAll()
.and()
.logout()
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
ResponseData ret = ResponseData.error(99999, "登陆失败");
//判断异常的类型
if (exception instanceof LockedException) {
ret.setMsg("账号被锁定");
} else if (exception instanceof CredentialsExpiredException) {
ret.setMsg("密码过期");
} else if (exception instanceof DisabledException) {
ret.setMsg("账号被禁用");
} else if (exception instanceof BadCredentialsException) {
ret.setMsg("账号或密码错误");
}
out.write(new ObjectMapper().writeValueAsString(ret));
out.flush();
out.close();
}
})
.permitAll()
.and()
.csrf().disable(); //屏蔽跨域攻击
}
}TestBcryptPassword
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes= {App.class})
public class TestBcryptPassword {
@Test
public void testBcryptPassword() {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String password = "123";
String str = encoder.encode(password);
System.out.println(str);
}
}如何实现无状态
无状态登录的流程
- 首先客户端发送账户名/密码到服务端进行认证
- 认证通过后,服务端将用户信息加密并且编码成一个token,返回给客户端
- 以后客户端每次发送请求,都需要携带认证的token
- 服务端对客户端发送来的token进行解密,判断是否有效,并且获取用户登录信息
1. 应用程序或客户端向授权服务器请求授权
2. 获取到授权后,授权服务器会向应用程序返回访问令牌
3. 应用程序使用访问令牌来访问受保护资源(如API)
因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务端就无需保存用户信息,甚至无需去数据库查询,这样就完全符合了RESTful的无状态规范.JWT数据格式
JWT包含三部分数据:
Header
头部,通常头部有两部分信息:
- 声明类型,这里是JWT
- 加密算法,自定义
我们会对头部进行Base64Url编码(可解码),得到第一部分数据
Payload
载荷,就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:
- iss(issuer): 表示签发人
- exp(expiration time): 表示token过期时间
- sub(subject): 主题
- aud(audience): 受众
- nbf(Not Before): 生效时间
- iat(Issued At): 签发时间
- jti(JWT ID): 编号
这部分也会采用Base64Url编码,得到第二部分数据
Signature
签名,是整个数据的认证信息.
一般根据前两步的数据,再加上服务的密钥secret(密钥保存在服务端,不能泄露给客户端),通过Header中配置的加密算法生成.
用于验证整个数据完整和可靠性.
JWT存在的问题
续签问题,这是被很多人诟病的问题之一,传统的cookie+session的方案天然的支持续签,但是jwt由于服务端不保存用户状态,因此很难完美解决续签问题
如果引入redis,虽然可以解决问题,但是jwt也变得不伦不类了
注销问题,由于服务器端不再保存用户信息,所以一般可以通过修改secret来实现注销,服务端secret修改后,已经颁发的未过期的token就会认证失败,进而实现注销,不过毕竟没有传统的注销方便
密码重置,密码重置后,原本的token依然可以访问系统,这时候也需要强制修改secret
基于第2点和第3点,一般建议不同用户取不同secret
Test
SecurityConfig
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SysUserService userService;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("账号:" + username);
SysUser user = userService.findUserByUsercode(username);
if (user != null) {
Collection<? extends GrantedAuthority> authorities = new ArrayList<>();
return new UserDetailImpl(user, authorities);
}
throw new UsernameNotFoundException("账号或密码错误");
}
};
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) //无状态请求
.and()
.authorizeRequests()
.antMatchers("/doLogin")
.permitAll()
.anyRequest()
.authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter("/doLogin", authenticationManager()), UsernamePasswordAuthenticationFilter.class) //用户认证,成功和失败的回调
.addFilterBefore(null, UsernamePasswordAuthenticationFilter.class); //资源拦截的认证
http.csrf().disable();
http.headers().cacheControl();
http.exceptionHandling().accessDeniedHandler(null)
.authenticationEntryPoint(null);
}
}JwtAuthenticationFilter
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JwtAuthenticationFilter(String url, AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
setFilterProcessesUrl(url);
}
//用户认证
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
//获取前端Json的数据(用户名和密码)
SysUser user = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);
System.out.println("用户名:" + user.getUsercode());
System.out.println("密码" + user.getPassword());
// subject.login(token)
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsercode(), user.getPassword(), new ArrayList<>()));
} catch (Exception e) {
e.printStackTrace();
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(null, null, new ArrayList<>()));
}
}
//成功认证的回调方法: 主要是返回一个有效的jwt的token
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
UserDetailImpl userDetailImpl = (UserDetailImpl) authResult.getPrincipal();
//生成token
String token = JwtTokenUtil.createToken("gec", userDetailImpl.getUsername(), 1800L);
System.out.println(token);
//放在header
response.setHeader("token", token);
response.setCharacterEncoding("utf-8");
out.write(new ObjectMapper().writeValueAsString(CommonResult.success(token)));
out.flush();
out.close();
}
//认证失败的回调方法
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
System.out.println("failed exception:" + failed.getMessage());
out.write(new ObjectMapper().writeValueAsString(CommonResult.failed("登录失败")));
out.flush();
out.close();
}
}JwtTokenUtil
import java.util.Date;
import java.util.Optional;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
public class JwtTokenUtil {
public static final String TOKEN_HEADER = "gtboot";
//public static final String TOKEN_PREFIX = "gtboot ";
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 密钥
*/
private static final String SECRET = "jwt_secret_gtboot";
private static final String ISS = "gtboot";
/**
* 过期时间是 1800 秒
*/
private static final long EXPIRATION = 1800L;
public static String createToken(String issuer, String subject, long expiration) {
return createToken(issuer, subject, expiration, null);
}
/**
* 创建 token
*
* @param issuer 签发人
* @param subject 主体,即用户信息的JSON
* @param expiration 有效时间(秒)
* @param claims 自定义参数
* @return
* @description todo https://www.cnblogs.com/wangshouchang/p/9551748.html
*/
public static String createToken(String issuer, String subject, long expiration, Claims claims) {
return Jwts.builder()
// JWT_ID:是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
// .setId(id)
// 签名算法以及密匙
.signWith(SignatureAlgorithm.HS512, SECRET)
// 自定义属性
.setClaims(null)
// 主题:代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.setSubject(subject)
// 受众
// .setAudience(loginName)
// 签发人
.setIssuer(Optional.ofNullable(issuer).orElse(ISS))
// 签发时间
.setIssuedAt(new Date())
// 过期时间
.setExpiration(new Date(System.currentTimeMillis() + (expiration > 0 ? expiration : EXPIRATION) * 1000))
.compact();
}
/**
* 从 token 中获取主题信息
*
* @param token
* @return
*/
public static String getProperties(String token) {
return getTokenBody(token).getSubject();
}
/**
* 校验是否过期
*
* @param token
* @return
*/
public static boolean isExpiration(String token) {
return getTokenBody(token).getExpiration().before(new Date());
}
/**
* 获得 token 的 body
*
* @param token
* @return
*/
private static Claims getTokenBody(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
}