鉴权框架的实现都是两大步认证和授权,我们分别来说说思路和具体的代码实现
认证
SpringSecurity的认证是通过一系列的过滤链实现的,先上原理图
其中认证分为两步:登录和验证
登录
注:(这里只有关键代码,全部的代码在文章末尾会上git链接)
我们这里不用springsecurity自带的登录接口实现,
先自定义controller,然后再serviceimpl中调用过滤链。
package com.wwj.service.impl;
import com.wwj.constant.SecurityConstants;
import com.wwj.domain.AjaxResult;
import com.wwj.domain.LoginUser;
import com.wwj.domain.SysUser;
import com.wwj.domain.WXAuthenticationToken;
import com.wwj.service.LoginService;
import com.wwj.utils.JwtUtils;
import com.wwj.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
/**
* 登陆接口实现类
*/
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
@Qualifier("myAuthenticationManager")
AuthenticationManager authenticationManager;
@Autowired
RedisCache redisCache;
/**
* 用户名密码登陆
* @param sysUser
* @return
*/
@Override
public AjaxResult login(SysUser sysUser) {
//选则security带的token(继承Authentication)需要赋值用户名密码
//选用不同的token,会通过循环找到token对应的provider,利用provider进行验证
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(sysUser.getUserName(),sysUser.getPassword());
String token = this.authenticate(usernamePasswordAuthenticationToken);
return AjaxResult.success("登录成功",token);
}
/**
* 微信登陆(利用id登陆)
* @param sysUser
* @return
*/
@Override
public AjaxResult wxLogin(SysUser sysUser) {
//选则自定义的微信token(继承Authentication)只赋值openid即可
//选用不同的token,会通过循环找到token对应的provider,利用provider进行验证
WXAuthenticationToken wxAuthenticationToken = new WXAuthenticationToken(sysUser.getOpenId());
String token = this.authenticate(wxAuthenticationToken);
return AjaxResult.success("登录成功",token);
}
/**
* 退出接口
* @return
*/
@Override
public AjaxResult loginOut() {
//获取SecurityContextHolder的用户id
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//删除redis值
redisCache.deleteObject("login:"+loginUser.getSysUser().getUserId());
return AjaxResult.success("退出成功");
}
/**
* AuthenticationManager调用proverManager去寻找合适的验证方式去选择不同的proverder
* return token
*/
private String authenticate(Authentication authentication){
Authentication authenticate = authenticationManager.authenticate(authentication);
//认证通过,使用userID生成token,再将token返回前端
LoginUser loginUser = (LoginUser)authenticate.getPrincipal();
SysUser sysUser1 = loginUser.getSysUser();
//若果认证没通过给出提示
if(sysUser1 == null){
throw new RuntimeException("登录失败");
}
HashMap<String, Object> claims = new HashMap<>();
claims.put(SecurityConstants.DETAILS_USER_ID,sysUser1.getUserId());
String token = JwtUtils.createToken(claims);
//完整的用户信息存入redis userId作为key
redisCache.setCacheObject("login:"+sysUser1.getUserId(),loginUser);
return token;
}
}
主要方法Authentication authenticate = authenticationManager.authenticate(authentication);
通过配置authenticationManager调用providerManger再通过循环具体根据传入的token类型选择合适的provider进行用户名密码的验证处理
package com.wwj.config;
import com.wwj.filter.JwtAuthenticationTokenFilter;
import com.wwj.provider.WXProvider;
import com.wwj.service.impl.UserNameDetailServiceImpl;
import com.wwj.service.impl.UserOpenIdDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.Arrays;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserNameDetailServiceImpl userNameDetailServiceImpl;
@Autowired
private UserOpenIdDetailServiceImpl userOpenIdDetailServiceImpl;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 配置provider(DaoAuthenticationProvider) 用户名密码的一套
* @return
*/
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider(){
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(userNameDetailServiceImpl);
return daoAuthenticationProvider;
}
/**
* 配置provider(WXProvider) 微信的一套(根据openid直接登入系统)
* @return
*/
@Bean
public WXProvider getWXProvider(){
WXProvider wxProvider = new WXProvider();
wxProvider.setUserOpenIdDetailsService(userOpenIdDetailServiceImpl);
return wxProvider;
}
/**
* 创建密码加密方式BCryptPasswordEncoder注入容器中
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 定义认证管理器
* @return
* @throws Exception
*/
@Override
@Bean(name="myAuthenticationManager")
public AuthenticationManager authenticationManagerBean() throws Exception {
ProviderManager authenticationManager = new ProviderManager(Arrays.asList(daoAuthenticationProvider(),getWXProvider()));
//不擦除认证密码,擦除会导致TokenBasedRememberMeServices因为找不到Credentials再调用UserDetailsService而抛出UsernameNotFoundException
authenticationManager.setEraseCredentialsAfterAuthentication(false);
return authenticationManager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//登录接口允许匿名访问
.antMatchers("/login/**").anonymous()
.anyRequest().authenticated();
http
//添加token认证的过滤器到security的过滤链前
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
这里自定义了微信用的provider
package com.wwj.provider;
import com.wwj.domain.LoginUser;
import com.wwj.domain.WXAuthenticationToken;
import com.wwj.service.UserOpenIdDetailsService;
import lombok.Data;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
/**
* 微信调用的provider
*/
@Data
public class WXProvider implements AuthenticationProvider {
private UserOpenIdDetailsService userOpenIdDetailsService;
/**
* 取到authentication中的openId,根据openId查询信息,能查到信息表示登陆成功
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
LoginUser loginUser = (LoginUser) userOpenIdDetailsService.loadUserByOpenId(authentication.getPrincipal().toString());
if(loginUser == null){
throw new RuntimeException("登陆失败");
}
WXAuthenticationToken wxAuthenticationToken = new WXAuthenticationToken(loginUser);
return wxAuthenticationToken;
}
/**
* 配置当前Provider对应的wxAuthenticationToken
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return (WXAuthenticationToken.class.isAssignableFrom(authentication));
}
}
同时配置了微信用的service
package com.wwj.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.wwj.domain.LoginUser;
import com.wwj.domain.SysUser;
import com.wwj.mapper.UserMapper;
import com.wwj.service.UserOpenIdDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 根基openid获取用户信息
*/
@Service
public class UserOpenIdDetailServiceImpl implements UserOpenIdDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByOpenId(String openId) throws UsernameNotFoundException {
//根据用户名在数据库中查询用户信息
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("open_id", openId);
SysUser sysUser = userMapper.selectOne(queryWrapper);
//TODO 先写死
List<String> permissions = new ArrayList<>(Arrays.asList("admin","common"));
//再封装成UserDetails实现类
LoginUser loginUser = new LoginUser(sysUser,permissions);
loginUser.setSysUser(sysUser);
return loginUser;
}
}
通过上面部分代码,我们来就有了两条登录验证的走法
1.用户名密码登录
LoginController >>> LoginServiceImpl >>>authenticationManager(UsernamePasswordAuthenticationToken) >>> ProviderManager>>>daoAuthenticationProvider>>>UserNameDetailServiceImpl
2.微信用openid登录
LoginController >>> LoginServiceImpl >>>authenticationManager(WXAuthenticationToken) >>> ProviderManager>>>WXProvider>>>UserOpenIdDetailServiceImpl
登录成功后会将userid变成token,同时将userid为key,用户的信息为value存入redis中
验证
然后每一次访问接口都需要携带token,解析token,取redis的用户详细信息,放到SpringSecurity的容器中
定义一条过滤链,配置在spring过滤链前
package com.wwj.filter;
import com.alibaba.fastjson.JSON;
import com.wwj.constant.SecurityConstants;
import com.wwj.domain.LoginUser;
import com.wwj.utils.JwtUtils;
import com.wwj.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if(!StringUtils.hasText(token)){
//放行
filterChain.doFilter(request,response);
return;
}
//解析token
Claims claims = JwtUtils.parseToken(token);
String userId = claims.get(SecurityConstants.DETAILS_USER_ID).toString();
//根据userId在redis中获取用户信息
LoginUser loginUser = redisCache.getCacheObject("login:" + userId);
if(loginUser == null){
throw new RuntimeException("用户未登录");
}
//存入SecurityContex上下文
//TODO 获取权限信息
UsernamePasswordAuthenticationToken userToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(userToken);
//放行
filterChain.doFilter(request,response);
}
}
授权
登录成功后,会在SpringSecurity的容器中取到用户的角色信息,根据接口所需要的的角色信息进行匹配,匹配成功后即可放行
实现授权主要有两种方式:
通过配置类进行配置(主要配置静态资源的放行)
通过注解方式实现对每个接口的授权认证
package com.wwj.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;1
@RestController
public class HelloController {
@GetMapping("hello")
@PreAuthorize("hasAuthority('test')")
public String hello(){
return "hello";
}
@GetMapping("hello1")
@PreAuthorize("hasAuthority('admin')")
public String hello1(){
return "hello1";
}
@GetMapping("hello2")
public String hello2(){
return "hello2";
}
}
好啦,springsecurity到这里就实现成功啦。以上的文档为简略说明思路,具体代码查看
https://gitee.com/c_wolun/security-demo.git