说明:
SpringSecurity是一个用于Java 企业级应用程序的安全框架,主要包含用户认证和用户授权两个方面.相比较Shiro而言,Security功能更加的强大,它可以很容易地扩展以满足更多安全控制方面的需求,但也相对它的学习成本会更高,两种框架各有利弊.实际开发中还是要根据业务和项目的需求来决定使用哪一种.
JWT是在Web应用中安全传递信息的规范,从本质上来说是Token的演变,是一种生成加密用户身份信息的Token,特别适用于分布式单点登陆的场景,无需在服务端保存用户的认证信息,而是直接对Token进行校验获取用户信息,使单点登录更为简单灵活.
Maven相关依赖如下:
<!--Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
属性配置如下:
# JWT配置
# 密匙KEY
secret: 123456
# HeaderKEY
tokenHeader: Authorization
# Token前缀字符
tokenPrefix: fhpt-
# 过期时间 单位毫秒 20分钟
expiration: 7600000
#expiration: 40000
# 配置不需要认证的接口
antMatchers: /user/**,/dataSet/sampleExcel/download,/doc.html
JWTConfig :
@Component
public class JWTConfig {
@Value("${secret}")
public String secret;
@Value("${tokenHeader}")
public String tokenHeader;
@Value("${tokenPrefix}")
public String tokenPrefix;
@Value("${expiration}")
public String expiration;
@Value("${antMatchers}")
public String antMatchers;
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getTokenHeader() {
return tokenHeader;
}
public void setTokenHeader(String tokenHeader) {
this.tokenHeader = tokenHeader;
}
public String getTokenPrefix() {
return tokenPrefix;
}
public void setTokenPrefix(String tokenPrefix) {
this.tokenPrefix = tokenPrefix;
}
public String getExpiration() {
return expiration;
}
public void setExpiration(String expiration) {
this.expiration = expiration;
}
public String getAntMatchers() {
return antMatchers;
}
public void setAntMatchers(String antMatchers) {
this.antMatchers = antMatchers;
}
}
JWT工具类:
/**
* JWT工具类
*/
@Slf4j
public class JWTTokenUtil {
private static JWTConfig jwtConfig=null;
public static JWTConfig getJWTConfig(){
if(jwtConfig==null){
jwtConfig = SpringContextUtil.getBeanByClass(JWTConfig.class);
}
return jwtConfig;
}
/**
* 生成Token
* @Param userInfo 用户安全实体
* @Return Token
*/
public static String createAccessToken(UserInfo userInfo){
// 登陆成功生成JWT
String expiration =getJWTConfig().getExpiration();
Integer expirate =StringUtils.isEmpty(expiration)?86400:Integer.parseInt(expiration);
log.info("创建token: userId:{}, userCode:{}",userInfo.getId(),userInfo.getUserCode());
String token = Jwts.builder()
// 放入用户名和用户ID
.setId(userInfo.getId()+"")
// 主题
.setSubject(userInfo.getUserCode())
// 签发时间,可选
//.setIssuedAt(new Date())
// 签发者,可选
//.setIssuer("xie")
.claim("groupId",userInfo.getGroupId())
// 自定义属性 放入用户拥有权限
.claim("authorities", JSON.toJSONString(userInfo.getAuthorities()))
//.claim("groupId",)
// 失效时间
.setExpiration(new Date(System.currentTimeMillis() + expirate))
// 签名算法和密钥,可选
.signWith(SignatureAlgorithm.HS512, getJWTConfig().getSecret())
.compact();
return token;
}
}
JWT接口请求校验拦截器:
/**
* JWT接口请求校验拦截器
* 请求接口时会进入这里验证Token是否合法和过期
* @Author xieGuanglin
* @CreateTime 2020/5/12
*/
@Slf4j
public class JWTAuthenticationTokenFilter extends BasicAuthenticationFilter {
/**初始化配置类*/
private JWTConfig jwtConfig=null;
public JWTConfig getJWTConfig(){
if(jwtConfig==null){
jwtConfig = SpringContextUtil.getBeanByClass(JWTConfig.class);
}
return jwtConfig;
}
public JWTAuthenticationTokenFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//设置字符
response.setCharacterEncoding("utf-8");
String msg="登录过期,请重新登录";
// 获取前端请求头中JWT的Token
String tokenHeader = request.getHeader(getJWTConfig().getTokenHeader());
if (tokenHeader!=null && tokenHeader.startsWith(getJWTConfig().getTokenPrefix())) {
try {
// 获取token
String token = tokenHeader.replace(getJWTConfig().getTokenPrefix(), "");
// 解析JWT中token
Claims claims = Jwts.parser()
.setSigningKey(getJWTConfig().getSecret())
.parseClaimsJws(token)
.getBody();
// 获取用户名
String username = claims.getSubject();
String userId=claims.getId();
int groupId =(Integer) claims.get("groupId");
log.info("获取token中的用户名:{},用户id:{}",username,userId);
if(!StringUtils.isEmpty(username)&&!StringUtils.isEmpty(userId)) {
// 获取角色
List<GrantedAuthority> authorities = new ArrayList<>();
String authority = claims.get("authorities").toString();
if(!StringUtils.isEmpty(authority)){
List<Map<String,String>> authorityMap = JSONObject.parseObject(authority, List.class);
for(Map<String,String> role : authorityMap){
if(!StringUtils.isEmpty(role)) {
authorities.add(new SimpleGrantedAuthority(role.get("authority")));
}
}
}
//组装参数
UserInfo userInfo = new UserInfo();
userInfo.setUserCode(claims.getSubject());
userInfo.setId(Integer.parseInt(claims.getId()));
userInfo.setGroupId(groupId);
userInfo.setAuthorities(authorities);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userInfo, userId, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (ExpiredJwtException e){
//log.info("Token过期");
Result result =new Result(100,msg);
log.info("响应前端:{}", JSON.toJSONString(result));
response.getWriter().write(JSON.toJSONString(result));
//终止方法, 否则框架自动重定向到login页面,不适用前后端分离系统
return;
}
catch (Exception e) {
//log.info("Token无效");
Result result =new Result(100,msg);
log.info("响应前端:{}", JSON.toJSONString(result));
response.getWriter().write(JSON.toJSONString(result));
return;
}
}
filterChain.doFilter(request, response);
return;
}
}
自定义登录验证:
@Component
@Slf4j
public class UserAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserService userService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取表单输入中返回的用户名, 前端post请求, 默认用户名参数: username
String userCode =authentication.getName();
//或log.info("用户名:{}", (String) authentication.getPrincipal());
if(StringUtils.isEmpty(userCode)){
throw new UsernameNotFoundException("请输入用户名");
}
// 获取表单中输入的密码
String password = (String) authentication.getCredentials();
// 查询用户是否存在
UserEntity userEntity = userService.loginByUserCode(userCode);
if (userEntity ==null ) {
throw new UsernameNotFoundException("用户名不存在");
}
// 我们还要判断密码是否正确,这里我们的密码使用BCryptPasswordEncoder进行加密的
if(!MD5Util.getMD5Str(password).equals(userEntity.getPassword())){
throw new BadCredentialsException("密码不正确");
}
/*if (!new BCryptPasswordEncoder().matches(password, userEntity.getPassword())) {
throw new BadCredentialsException("密码不正确");
}*/
// 还可以加一些其他信息的判断,比如用户账号已停用等判断
if (0==userEntity.getStatus()){
throw new LockedException("该用户已被冻结");
}
UserInfo userInfo=new UserInfo();
BeanUtils.copyProperties(userEntity, userInfo);
Integer curGroupId =userService.getCurrentGroupId(userEntity.getId());
userInfo.setGroupId(curGroupId);
// 角色集合
List<GrantedAuthority> authorities = new ArrayList<>();
// 查询用户角色
List<RoleEntity> roleEntityList = userService.selectRoleById(userEntity.getId());
for (RoleEntity roleEntity: roleEntityList){
log.info("用户:{} 拥有权限:{}",userCode ,"ROLE_" + roleEntity.getRoleName());
authorities.add(new SimpleGrantedAuthority("ROLE_" + roleEntity.getRoleName()));
}
userInfo.setAuthorities(authorities);
// 进行登录
return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return true;
}
}
暂无权限处理类:
@Component
@Slf4j
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
log.info(httpServletRequest.getRequestURL().toString());
Result result =new Result(100,"请登录");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
用户未登录处理类:
@Component
@Slf4j
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
log.info(httpServletRequest.getRequestURL().toString());
Result result =new Result(100,"请登录");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
登录成功处理类:
/**
* @Description 登录成功处理类
*/
@Component
@Slf4j
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {
@Value("${secret}")
private String secret;
@Value("${tokenHeader}")
private String tokenHeader;
@Value("${tokenPrefix}")
private String tokenPrefix;
@Autowired
private GroupRepository groupRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// 组装JWT
UserInfo userInfo = (UserInfo) authentication.getPrincipal();
String token = JWTTokenUtil.createAccessToken(userInfo);
token = tokenPrefix + token;
Integer groupId =userInfo.getGroupId();
// 封装返回参数
Map<String,Object> resultData = new HashMap<>();
resultData.put("code",0);
resultData.put("msg", "登录成功");
resultData.put("groupId",groupId);
resultData.put("token",token);
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(resultData));
}
}
登录失败处理类:
@Slf4j
@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
String msg =e.getMessage();
Result result =new Result(500,e.getMessage());
// log.info("返回异常信息:{}",e.getMessage());
// 这些对于操作的处理类可以根据不同异常进行不同处理
if (e instanceof UsernameNotFoundException){
msg ="用户名不存在";
}
if (e instanceof LockedException){
msg ="用户被冻结";
}
if (e instanceof BadCredentialsException){
msg ="用户名密码不正确";
}
log.info("【登录失败】"+msg);
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
用户登出:
@Component
public class UserLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
SecurityContextHolder.clearContext();
Result result =new Result(0,"登出成功");
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
自定义权限注解类:
@Slf4j
@Component
public class UserPermissionEvaluator implements PermissionEvaluator {
@Autowired
private UserService userService;
/**
* hasPermission鉴权方法
* 这里仅仅判断PreAuthorize注解中的权限表达式
* 实际中可以根据业务需求设计数据库通过targetUrl和permission做更复杂鉴权
* 当然targetUrl不一定是URL可以是数据Id还可以是管理员标识等,这里根据需求自行设计
* @Param authentication 用户身份(在使用hasPermission表达式时Authentication参数默认会自动带上)
* @Param targetUrl 请求路径
* @Param permission 请求路径权限
* @Return boolean 是否通过
*/
@SneakyThrows
@Override
public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {
// 获取用户信息
UserInfo userEntity =(UserInfo) authentication.getPrincipal();
// 查询用户权限(这里可以将权限放入缓存中提升效率)
Set<String> permissions = new HashSet<>();
log.info("targetUrl:{},权限:{}",targetUrl,permission.toString());
// 权限对比
permissions.add("select");
permissions.add("update");
permissions.add("insert");
boolean flag=false;
if (permissions.contains(permission.toString())){
return true;
}
log.info("用户:{}没有权限访问",userEntity.getUserCode());
//SecurityContextHolder.getContext().getAuthentication().s
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable serializable, String s, Object o) {
return false;
}
}
无权限处理类:
@Component
@Slf4j
public class UserAuthAccessDeniedHandler extends Throwable implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
log.info("未授权操作:"+e.getMessage());
Result result =new Result(403,"未授权");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
SpringSecurity核心配置类:
/**
* SpringSecurity核心配置类
*/
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JWTConfig jWTConfig;
/**
* 自定义登录成功处理器
*/
@Autowired
private UserLoginSuccessHandler userLoginSuccessHandler;
/**
* 自定义登录失败处理器
*/
@Autowired
private UserLoginFailureHandler userLoginFailureHandler;
/**
* 自定义注销成功处理器
*/
@Autowired
private UserLogoutSuccessHandler userLogoutSuccessHandler;
/**
* 自定义暂无权限处理器
*/
@Autowired
private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
/**
* 自定义未登录的处理器
*/
@Autowired
private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
/**
* 自定义登录逻辑验证器
*/
@Autowired
private UserAuthenticationProvider userAuthenticationProvider;
/**
* 加密方式
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 注入自定义PermissionEvaluator
*/
@Bean
public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler(){
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setPermissionEvaluator(new UserPermissionEvaluator());
return handler;
}
/**
* 配置登录验证逻辑
*/
@Override
protected void configure(AuthenticationManagerBuilder auth){
//这里可启用我们自己的登陆验证逻辑
auth.authenticationProvider(userAuthenticationProvider);
}
/**
* 配置security的控制逻辑
* @Param http 请求
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//不进行权限验证的请求或资源(从配置文件中读取)
.antMatchers(jWTConfig.antMatchers.split(",")).permitAll()
//其他的需要登陆后才能访问
.anyRequest().authenticated()
.and()
//配置未登录自定义处理类
.httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
.and()
//配置登录地址
.formLogin()
.loginProcessingUrl("/user/login")
//配置登录成功自定义处理类
.successHandler(userLoginSuccessHandler)
//配置登录失败自定义处理类
.failureHandler(userLoginFailureHandler)
.and()
//配置登出地址
.logout()
.logoutUrl("/user/logout")
//配置用户登出自定义处理类
.logoutSuccessHandler(userLogoutSuccessHandler)
.and()
//配置没有权限自定义处理类
.exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler)
.and()
// 开启跨域
.cors()
.and()
// 取消跨站请求伪造防护
.csrf().disable();
// 基于Token不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 禁用缓存
http.headers().cacheControl();
// 添加JWT过滤器
http.addFilter(new JWTAuthenticationTokenFilter(authenticationManager()));
}
}
最后控制层controller调用:
//方法上加权限注解
//拥有ADMIN
@PreAuthorize("hasRole('ADMIN')")
//拥有ADMIN或者USER角色可以访问
@PreAuthorize("hasAnyRole('ADMIN','USER')")
//拥有ADMIN和USER角色可以访问
@PreAuthorize("hasRole('ADMIN') and hasRole('USER')")
//拥有select权限可以访问
//hasPermission 第一个参数是请求路径 第二个参数是权限表达式
@PreAuthorize("hasPermission('/admin/userList','select')")
/*@PreAuthorize("hasRole('ADMIN') and hasPermission('/modelInfo/view','select')")*/
//拥有ADMIN角色和update权限可以访问
@PreAuthorize("hasRole('ADMIN') and hasPermission('/modelInfo/update','update')")
@GetMapping("/view")
public Result view(@RequestParam(value = "id") long id) {
Result result = new Result<>();
return result;
}
最后欢迎大家访问和使用我的个人网站:助记宝