SpringSecurity是什么
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。(引用百度百科)
为什么使用SpringSecurity
可以说SpringBoot的快速发展也使得了SpringSecurity的热度往上涨,在SpringBoot以前,Shiro和SpringSecurity的框架都是主流的安全框架,但Security的配置非常臃肿,性能也相对慢一些,最大的缺点还是必须依赖于Spring框架才能开发,不过唯一值得一提的就是Security默认实现了更多功能,更是提供了oauth授权的实现。不过一切的改变还是源于SpringBoot的出现,SpringBoot整合SpringSecurity的步骤非常简单,只需要继承WebSecurityConfigurerAdapter这个类并实现认证方法,接着配置一下登录的uri即可完成一个简单的用户认证功能,可以说整个功能都不用5分钟就能实现。我个人还是比较喜欢SpringBoot全家桶,不需要解决框架整合上的小麻烦,功能上来说也很强大。
JWT
JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
怎么使用
使用JWT,我们只需要在请求的请求头上添加如图下类似的数据(token)。后端根据需要认证的url进行拦截,取出Hearders里面的数据,紧接着解析出这段token的包含的信息,判断信息是否正确即可。token其实就是根据信息加密而来的一段字符串,我们将需要用到的信息放到token中,token包含的信息尽可能的简洁。
image.png
注意:
虽然简单的jwt认证并没有什么难度,但如果你没使用过SpringSecurity,建议还是先去简单的学习一下。
开始
- 编写通过用户id或用户手机号码查询User和Role的方法
- 编写Token生成工具类
- 继承UserDetails接口
- 继承UserDetailsService接口,实现用户认证方法
- 编写用户账号验证失败处理器与权限不足处理器
- 编写Token验证过滤器
- 配置SpringSecurity Config
- 实现登录方法
整个流程还是相对完善的,所以步骤稍多
导入jar
这里需要提一下的就是,当你引入这个包的时候,SpringBoot默认会为项目所有的请求添加认证,这也是SpringSecurity的常规操作,如果你还不知道的话,赶快刹车调头回家补课。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
实现用户登录方法
用户通过手机号及密码进行登录,我们需要先获取用户的身份信息以及角色信息
UserMapper.xml
<resultMap id="User_Role" type="com.viu.technology.po.User">
<id property="id" column="id" javaType="java.lang.String" jdbcType="BIGINT" />
<result property="name" column="name" javaType="java.lang.String" jdbcType="VARCHAR" />
<result property="phone" column="phone" javaType="java.lang.String" jdbcType="VARCHAR" />
<result property="password" column="password" javaType="java.lang.String" jdbcType="VARCHAR" />
<collection property="roles" ofType="com.viu.technology.po.Role">
<id property="id" column="role_id" jdbcType="BIGINT"/>
<result property="roleName" column="role_name" jdbcType="VARCHAR" />
</collection>
</resultMap>
<select id="selUserAndRoleByPhone" parameterType="java.lang.String" resultMap="User_Role">
SELECT u.id,u.name,u.password,u.phone,r.id as role_id ,r.role_name
from t_user u
LEFT JOIN t_role r on u.id=r.user_id
where u.phone=#{phone,jdbcType=VARCHAR}
</select>
<select id="selUserAndRoleById" parameterType="java.lang.String" resultMap="User_Role">
SELECT u.id,u.name,u.password,u.phone,r.id as role_id ,r.role_name
from t_user u
LEFT JOIN t_role r on u.id=r.user_id
where u.id=#{id,jdbcType=VARCHAR}
</select>
UserMapper.java
User selUserAndRoleByPhone(String phone);
User selUserAndRoleById(String id);
UserDao.java
User selUserAndRoleByPhone(String phone);
User selUserAndRoleById(String id);
UserDaoImpl.java
public User selUserAndRoleByPhone(String phone) {
User user = userMapper.selUserAndRoleByPhone(phone);
return user;
}
public User selUserAndRoleById(String id){
User user = userMapper.selUserAndRoleById(id);
return user;
}
UserService.java
User getUserAndRoleByPhone(String phone);
User getUserAndRoleById(String id);
UserServiceImpl.java
public User getUserAndRoleByPhone(String phone) {
User user = userDao.selUserAndRoleByPhone(phone);
return user;
}
public User getUserAndRoleById(String id) {
User user = userDao.selUserAndRoleById(id);
return user;
}
操作数据库获取用户身份信息的代码就到此为止了,接下来就开始编写SpringSecurity+jwt的认证代码了
编写Token生成工具类----JwtTokenUtil
工具类主要用作生成token、刷新token以及验证token。Token和Session一个很大的区别就是无登录状态,我们可以利用清除session做登出的操作,但无法利用token直接做登出操作,后续会进行讲解。
这个token里的信息比较简单,只存放了sub和create,你可以根据自己业务需求在generateToken(UserDetails userDetails)方法里面添加不同的数据即可,后续通过getClaimsFromToken方法获取Claims对象,接着调用Claims对象的get方法获取出对应的数据即可。
@Component
public class JwtTokenUtil{
/**
* 密钥
*/
private static final String secret = "lkhouhubkljgpihojblkjboiboihu9u";
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
public static String generateToken(Map<String, Object> claims) {
//设置token的有效期为24*7小时,也就是一周
Date expirationDate = new Date(System.currentTimeMillis() +60*60*24*7 * 1000);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
public static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成令牌
*
* @param userDetails 用户
* @return 令牌
*/
public static String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(2);
claims.put("sub", userDetails.getUsername());
claims.put("created", new Date());
return generateToken(claims);
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public static String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public static Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public static String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put("created", new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 验证令牌
*
* @param token 令牌
* @param userDetails 用户
* @return 是否有效
*/
public static Boolean validateToken(String token, UserDetails userDetails) {
String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
继承UserDetails接口
UserDetails接口是SpringSecurity框架用于认证授权的一个载体,只有实现了这个接口的类才能被SpringSecurity验证,
public class User implements UserDetails {
private String id;
private String name;
private String password;
private String phone;
private List<Role> roles;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
public User(String id, String name, String password, String phone) {
this.id = id;
this.name = name;
this.password = password;
this.phone = phone;
}
public User() {
super();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//获取用户角色权限,此处从数据库表Role中获取
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> auths = new ArrayList<>();
List<Role> roles = getRoles();
if (roles!=null) {
for (Role role : roles) {
auths.add(new SimpleGrantedAuthority(role.getRoleName()));
}
}
return auths;
}
//这个是UserDetails默认实现获取密码的方法
@Override
public String getPassword() {
return password;
}
//这里getUsername翻译过来就是获取用户名的意思,但这个可以作为我们获取用户信息的一个标识
@Override
public String getUsername() {
return id;
}
//用户账号是否过期,暂时没这个功能,默认返回true,即未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//用户账号是否锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//用户凭证是否过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//账号是否可用
@Override
public boolean isEnabled() {
return true;
}
public void setPassword(String password) {
this.password = password;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
编写登录认证方法JwtUserDetailsServiceImpl.java
该类位于com.viu.technology.service.auth包下(自行建包)
JwtUserDetailsServiceImpl实现了UserDetailsService接口,SpringSecurity会去IOC容器中寻找实现这个接口的实现类,并将该实现类作为默认的认证类。这个类主要用于获取用户身份信息,并不需要我们去判断用户名和密码是否匹配。参照UserDetails实现的getPassword和getUsername方法。
这里之所要对username的长度进行判断是因为,我们登录的时候用的是手机号+明文密码进行登录,而保存在token里的信息只有id。登录方法和Token认证过滤器都会调用loadUserByUsername方法,所以需要做一个判断。可能会有一点疑问,既然是这样,为什么不直接用手机号做为token的传递信息就好了呢,主要还是因为我们使用手机号查询的情况比较少,而表的主键id才是经常用的。
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
@Autowired
@Lazy
private UserService userService;
public JwtUserDetailsServiceImpl(){
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = null;
if (username.length() == 32) {
user= userService.getUserAndRoleById(username);
} else if(username.length()==11) {
user= userService.getUserAndRoleByPhone(username);
}
log.info("user:" + user);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
}else{
return user;
}
}
}
编写账号密码验证失败处理器EntryPointUnauthorizedHandler.java
位于com.viu.technology.handler包下,自行创建
@Component
public class EntryPointUnauthorizedHandler implements AuthenticationEntryPoint {
private static Logger log = LoggerFactory.getLogger(EntryPointUnauthorizedHandler.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setStatus(401);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JsonUtil.objectToString(Result.fail(ResultCode.USER_LOGIN_FIAL)));
}
}
编写账户权限不足处理器RestAccessDeniedHandler.java
位于com.viu.technology.handler包下,自行创建
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setStatus(403);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JsonUtil.objectToString(Result.fail(ResultCode.USER_PERMISSION_DENIED)));
}
}
编写Token验证过滤器JwtAuthenticationTokenFilter.java
位于com.viu.technology.filter包下,自行创建
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
//该字符串作为Authorization请求头的值的前缀
String tokenHead = "tech-";
if (authHeader != null && authHeader.startsWith(tokenHead)) {
String authToken = authHeader.substring(tokenHead.length());
//从token中获取userId
String userId = JwtTokenUtil.getUsernameFromToken(authToken);
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//调用UserDetailsService的认证方法(JwtUserDetailsServiceImpl实现类)
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userId);
//验证token是否正确
if (JwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//将获取到的用户身份信息放到SecurityContextHolder中,这个类是为了在线程中保存当前用户的身份信息
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} else {
log.info("没有获取到token");
}
chain.doFilter(request, response);
}
}
配置SpringSecurity
位于com.viu.technology.config.security包下,自行创建
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启security方法级别权限控制注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
@Autowired
private RestAccessDeniedHandler restAccessDeniedHandler;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private SimpleUrlAuthenticationSuccessHandler successHandler;
@Autowired
private SimpleUrlAuthenticationFailureHandler failureHandler;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//这里的参数为不需要认证的uri,**代表匹配多级路径,*代表匹配一级路径,#代表一个字符....
.antMatchers(
"/demo/**",
"/user/generate/token"
).permitAll()
//这里表示该路径需要管理员角色
.antMatchers("/auth/test").hasAnyAuthority("管理员")
.anyRequest().authenticated()
.and()
.headers().cacheControl();
//添加认证过滤
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//添加权限不足及验证失败处理器
httpSecurity.exceptionHandling().authenticationEntryPoint(entryPointUnauthorizedHandler).accessDeniedHandler(restAccessDeniedHandler);
}
//这个为SpringSecurity的加密类
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
实现登录方法
UserService.java
String login(String phone, String password);
UserServiceImpl.java
这里需要注意一下,UsernamePasswordAuthenticationToken会自动将password进行加密之后再比对,而我们之前写的注册用户方法是以明文方式存入数据库的,并没有加密,所以我们需要修改一下用户注册方法,然后重新注册
public String login(String phone, String password) {
//将用户名和密码生成Token
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(phone, password);
//调用该方法时SpringSecurity会去调用JwtUserDetailsServiceImpl 进行验证
Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
User userDetails = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return JwtTokenUtil.generateToken(userDetails);
}
@Autowired
PasswordEncoder passwordEncoder;
public User registerUser(User user) {
//在插入数据库时将原密码进行加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
User userRes = userDao.insertUser(user);
Role roleRes = roleDao.insertRole(new Role("普通群众", user.getId()));
List list = new ArrayList();
list.add(roleRes);
if (null != userRes && null != roleRes) {
userRes.setRoles(list);
return user;
}
return null;
}
UserController.java
@PostMapping(value = "/generate/token")
public Result getToken(String phone, String password) throws AuthenticationException {
String token = userService.login(phone, password);
return Result.success(token);
}
测试获取token接口
获取token接着我们调用一下之前写的注册接口,发现没发注册,因为我们在SpringSecurity的配置中并没有开放这个接口的认证,自行添加。注册是不需要用户身份验证的,否则你让人家怎么注册嘛。。。
image.png
测试Token是否能正常使用
UserController.java
@GetMapping("/self/info")
public Result getUserSelfInfo() {
//由于通过验证后我们会把用户对象存到SecurityContextHolder中,所以这时候我们能通过下面这句代码获取到用户的身份信息
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return Result.success(user);
}
接下来测试一下,如果能够正常获取就代表成功,记住token前面要加tech-这个几个字符串,看不顺眼的话自己去改过滤器
温馨提醒
你们会发现读出来的数据和我稍微有点不一样对吧,哈哈哈哈,肯定啊,你们没有过滤一下敏感字段(密码我忘了过滤了0.0),在User类上加入@JSONField(serialize = false)注解即可,SpringBoot会将持有该注解的字段过滤不进行输出。
image.png
网友评论