SpringSecurity和Json Web Token的整合应用
SpringSecurity 学习要点:
1.用户在访问资源前,springsecurity会对其进行身份认证和权限认证,在认证都通过的条件下可以正常访问资源.
2.认证过程中,如何处理服务器认证成功和失败.
SpringSecurity快速入门要点
一.自定义登录逻辑
- @Configuration注解描述的类是spring的配置类,会在服务器启动时,优先加载,通常用来自定义配置
@Bean注解将注解通常会在@Configuration注解描述的类中描述方法,用于告诉spring框架这个方法的返回值会交给spring管理(控制反转IOC) - 定义UserDetailService接口实现类,自定义登陆逻辑,代码如下:
UserDetailService为SpringSecurity官方提供的登录逻辑处理对象,我们自己可以实现此接口,然后在对应的方法中进行登录逻辑的编写即可.
@Service //交给spring容器去管理
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
/**
* 当我们执行登录操作时,底层会通过过滤器等对象,调用这个方法
* @param username 这个参数为页面输出的用户名
* @return 一般是从数据库基于用户名查询到的用户信息
* @throws UsernameNotFoundException
*/
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) {
//1.基于用户名从数据库查询用户信息
Map<String, Object> userMap = userMapper.selectUserByUsername(username);
if (userMap==null) {
throw new UsernameNotFoundException("user not exists");
}
//查询用户权限信息并进行封装
List<String> userPermissons = userMapper.selectUserPermissions((Long) userMap.get("id"));
String password = (String)userMap.get("password");
User user = new User(username,
password,
AuthorityUtils
.createAuthorityList(userPermissons.toArray(new String[]{})));
System.out.println(user.toString());
return user;
说明:这里会将查询的用户信息封装传递给SpringSecurity框架
二:修改安全配置
自定义配置类让其实现接口WebSecurityConfigurerAdapter,并重写相关config方法,进行登陆的自定义设计.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
public void configure(HttpSecurity http) throws Exception {
//1.关闭跨域攻击
http.csrf().disable();
//2.配置form认证
http.formLogin()
//登录成功怎么处理(向客户端返回json)
.successHandler(successHandler())//成功
//登录失败怎么处理?(向客户端返回json)
.failureHandler(failureHandler());//失败
//假如某个资源必须认证才可访问,那没有认证怎么办?
http.exceptionHandling()
.authenticationEntryPoint(entryPoint());//提示要认证
// .accessDeniedHandler(null);//提示访问被拒绝
//3.所有资源都要认证
http.authorizeRequests().anyRequest().authenticated();
}
//登录成功以后的处理器
private AuthenticationSuccessHandler successHandler(){
return (request, response, authentication) -> {
HashMap<String, Object> map = new HashMap<>();
map.put("state",200);
map.put("message","login ok");
//创建一个令牌对象,JWT格式的令牌可以满足这个要求
HashMap<String, Object> jwtMap = new HashMap<>();
//获取用户对象,此对象为登录成功以后封装了登录信息的对象
User principal = (User) authentication.getPrincipal();
//获取用户权限封装到jwtMap中
jwtMap.put("username",principal.getUsername());//登录用户
ArrayList<Object> authoritiesList = new ArrayList<>();
Collection<GrantedAuthority> as = principal.getAuthorities();
for(GrantedAuthority a:as){
authoritiesList.add(a.getAuthority());
}
//上面遍历的新方式
// user.getAuthorities().forEach((authority)->{
// authorities.add(authority.getAuthority());
//
// });
jwtMap.put("authorities",authoritiesList);//[sys:res:retrieve,sys:res:create]
//创建token
String token = JwtUtils.generatorToken(jwtMap);
map.put("token",token);
WebUtils.writeJsonToClient(response,map);
};
}
//登录失败的处理器
private AuthenticationFailureHandler failureHandler(){
return ( request, response, authException) ->{//lambda 表达式
HashMap<String, Object> map = new HashMap<>();
map.put("state",500);
map.put("message","username or password error");
try {
WebUtils.writeJsonToClient(response,map);
} catch (Exception e) {
e.printStackTrace();
}
};
}
//假如没有登录访问资源时
private AuthenticationEntryPoint entryPoint(){
return ( request, response, authException) ->{
HashMap<String, Object> map = new HashMap<>();
map.put("state",500);
map.put("message","please Login");
//思考除了返回这些信息,还有返回什么?(JWT令牌)
WebUtils.writeJsonToClient(response,map);
};
}
在UserDetailServiceImpl中与数据库交互的dao类,与配置文件
@Mapper
public interface UserMapper {
@Select("select * from sys_user where username=#{username}")
Map<String,Object> selectUserByUsername(@Param("username") String name);
@Select(" select distinct m.permission " +
" from sys_user u left join sys_user_role ur on u.id=ur.user_id " +
" left join sys_role_menu rm on ur.role_id=rm.role_id " +
" left join sys_menu m on rm.menu_id=m.id " +
" where u.id=#{id}")
List<String> selectUserPermissions(@Param("id") Long id);
}
"""application.yml的数据库配置"""
spring:
datasource:
url: jdbc:mysql:///databaseName?serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: username
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
Spring认证和授权异常处理
异常类型
对于SpringSecurity框架而言,在实现认证和授权业务时,可能出现如下两大类型异常:
1)AuthenticationException (用户还没有认证就去访问某个需要认证才可访问的方法时,可能出现的异常,这个异常通常对应的状态码401)
2)AccessDeniedException (用户认证以后,在访问一些没有权限的资源时,可能会出现的异常,这个异常通常对应的状态吗为403)
异常处理规范
SpringSecurity框架给了默认的异常处理方式,当默认的异常处理方式不满足我们实际业务需求时,此时我们就要自己定义异常处理逻辑,编写逻辑时需要遵循如下规范:
1)AuthenticationEntryPoint:统一处理 AuthenticationException 异常
2)AccessDeniedHandler:统一处理 AccessDeniedException 异常.
Json Web Token 快速入门要点
两个工具类
一.基于随机盐生成token,包括创建token,解析token,判断token是否失效的类
public class JwtUtils {
private static String secret="AABBCCDDEEFF";
/**基于负载和算法创建token信息*/
public static String generatorToken(Map<String,Object> map){
return Jwts.builder()
.setClaims(map)
.setExpiration(new Date(System.currentTimeMillis()+30*60*1000))
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,secret)
.compact();
}
/**解析token获取数据*/
public static Claims getClaimsFromToken(String token){
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
/**判定token是否失效*/
public static boolean isTokenExpired(String token){
Date expiration = getClaimsFromToken(token).getExpiration();
return expiration.before(new Date());
}
}
二.向客户端输出Json串的工具类
public static void writeJsonToClient(HttpServletResponse response,
Map<String,Object> dateMap) throws IOException {
//1.设置响应数据的编码
response.setCharacterEncoding("utf-8");
//2.告诉浏览器响应数据的内容类型以及编码
response.setContentType("application/json;charset=utf-8");
//3.将数据转换为json格式
String jsonStr = new ObjectMapper().writeValueAsString(dateMap);
//4.获取输出流对象将json数据写到客户端
PrintWriter out = response.getWriter();
out.println(jsonStr);
out.flush();
}
在资源访问时,配置类,以及授权没有通过的解决方式
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//加这语句就可以让preAuthorize生效,开启controller安全验证
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//1.关闭跨域攻击
http.csrf().disable();
//2.设置拒绝处理器(不允许访问资源时,应该给出什么反馈)
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler());
//3.资源访问(所有资源在本项目中的访问不进行认证)
http.authorizeRequests().anyRequest().permitAll();
}
/**资源访问被拒绝的处理器*/
public AccessDeniedHandler accessDeniedHandler(){
return (request,response,e)->{
//1.构建响应信息
Map<String,Object> map=new HashMap<>();
map.put("state",403);
map.put("message","权限不足");
//2.将响应信息写到客户端
WebUtils.writeJsonToClient(response,map);
};
}
}
定义SpringMVC配置类
在mvc配置类中添加了一个TokenInterceptor 用来进行对访问的资源,判断请求头的token信息.
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
//1.从请求中获取token对象(如何获取取决于你传递token的方式:header,params)
String token=request.getHeader("token");
//2.验证token是否存在
if(token==null||"".equals(token))
throw new RuntimeException("请先登录");//WebUtils.writeJsonToClient
//3.验证token是否过期(将来可以直接去访问认证服务器,在那去校验,谁发的令牌,谁校验令牌)
if(JwtUtils.isTokenExpired(token))
throw new RuntimeException("登录超时,请重新登录");
//4.解析token中的认证和权限信息(一般存储在jwt格式中的负载部分)
Claims claims=JwtUtils.getClaimsFromToken(token);
List<String> list=(List<String>)
claims.get("authorities");//这个名字应该与创建token时,指定的权限名相同
//5.封装和存储认证和权限信息
//5.1构建UserDetail对象(用户身份的象征-类似于一张名片,微信的二维码)
UserDetails userDetails=User.builder()
.username((String)claims.get("username"))
.password("")
.authorities(list.toArray(new String[]{}))
.build();
System.out.println(userDetails.toString());
//5.2构建Security权限交互对象(记住,固定写法)
PreAuthenticatedAuthenticationToken authToken=
new PreAuthenticatedAuthenticationToken(
userDetails,//用户身份
userDetails.getPassword(),
userDetails.getAuthorities());
//5.3将权限交互对象与当前请求进行绑定
authToken.setDetails(new WebAuthenticationDetails(request));
//5.4.将认证后的token存储到Security上下文(会话对象)
SecurityContextHolder.getContext().setAuthentication(authToken);
return true;
}
}
controller层展示
在进行前端页面访问时,update和delete没有该权限,故会报出权限不足.
FAQ 分析
为什么会选择SpringSecurity?(功能强大,SpringBoot诞生后在配置方面做了大量的简化)
SpringSecurity中的加密方式你用的什么?(Bcrypt,底层基于随机盐方式对密码进行hash不可逆加密,更加安全,缺陷是慢)
SpringSecurity中你用过哪些API?(BcryptPasswordEncoder,UserDetailService,UserDetail,User,
AuthenticationSuccessHandler,AuthenticationFailureHandler,…)
为什么要进行权限控制?(防止非法用户破坏数据)
SpringSecurity进行权限控制的步骤(@EnableGlobalMethodSecurity,@PreAuthorize)