栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

【SpringSecurity】SpringSecurity基础

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【SpringSecurity】SpringSecurity基础

框架简介 概要

一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是Spring Security重要核心功能。

(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录

(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。

一般来说,常见的安全管理技术栈的组合是这样的:
SSM + Shiro
Spring Boot/Spring Cloud + Spring Security

权限管理相关概念
主体
英文单词:principal
使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系统谁就是主体。

认证
英文单词:authentication
权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证明自己是谁。
笼统的认为就是以前所做的登录操作。

授权
英文单词:authorization
将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的能力。
所以简单来说,授权就是给用户分配权限。
完成权限管理需要三个对象
用户:主要包含用户名、密码和当前用户的角色信息,可实现认证操作。
角色:主要包含角色名称,角色描述和当前角色拥有的权限信息,可实现授权操作。
权限:权限也可以称为菜单,主要包含当前权限名称,url地址等信息,可实现动态展示菜单。
注:这三个对象中,用户与角色是多对多的关系,角色与权限是多对多的关系,用户与权限没有直接关系,二者是通过角色来建立关联关系的。
二、初识SpringSecurity 2.1 SpringSecurity概念
Spring Security是spring采用AOP思想,基于servlet过滤器实现的安全框架。它提供了完善的认证机制和方法级的授权功能。
2.2 入门案例
1、Spring Security主要jar包功能介绍
spring-security-core.jar
核心包,任何Spring Security功能都需要此包。
spring-security-web.jar
web工程必备,包含过滤器和相关的Web安全基础结构代码。
spring-security-config.jar
用于解析xml配置文件,用到Spring Security的xml配置文件的就要用到此包。
spring-security-taglibs.jar
Spring Security提供的动态标签库,jsp页面可以用。
2、配置web.xml
 
 
    springSecurityFilterChain 
    org.springframework.web.filter.DelegatingFilterProxy  

 
    springSecurityFilterChain 
    
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    SysUser sysUser = userDao.findByName(username);
    if(sysUser==null){
        //若用户名不对,直接返回null,表示认证失败。
        return null;
    }
    
    //用户要已经拥有一些角色
    List authorities = new ArrayList<>();
    List roles = sysUser.getRoles();
    for (SysRole role : roles) {
        authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
    }
    //最终需要返回一个SpringSecurity的UserDetails对象,而UserDetail是一个接口,可以new一个它的实现类即User,{noop}表示不加密认证。
    //authorities是认证的所有角色的集合
    //{noop}后面的密码,SpringSecurity会认为是原文
    return new User(sysUser.getUsername(), "{noop}"+sysUser.getPassword(), authorities);
}
3、在SpringSecurity主配置文件中指定认证使用的业务对象

    
    

5.3 加密认证
1、在IOC容器中提供加密对象



    
        
        
    

2、修改认证方法
去掉{noop}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    SysUser sysUser = userDao.findByName(username);
    if(sysUser==null){
        //若用户名不对,直接返回null,表示认证失败。
        return null;
    }
    List authorities = new ArrayList<>();
    List roles = sysUser.getRoles();
    for (SysRole role : roles) {
        authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
    }
    //最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证。
    return new User(sysUser.getUsername(), sysUser.getPassword(), authorities);
}
3、修改添加用户的操作
@Service
@Transactional
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
    @Autowired
    private RoleService roleService;
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    @Override
    public void save(SysUser user) {
        //对密码进行加密,手动把数据库中的密码设置成密文,然后再入库
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        userDao.save(user);
    }
    //……
}
5.4 设置用户状态
1、源码分析
用户认证业务里,我们封装User对象时,选择了三个构造参数的构造方法,其实还有另一个构造方法:
public User(String username, String password, boolean enabled, boolean accountNonExpired,
            boolean credentialsNonExpired, boolean accountNonLocked, Collection
            authorities) {
    if (username != null && !"".equals(username) && password != null) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.credentialsNonExpired = credentialsNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
    } else {
        throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
    }
}
可以看到,这个构造方法里多了四个布尔类型的构造参数,其实我们使用的三个构造参数的构造方法里这四个布尔值默认都被赋值为了true,那么这四个布尔值到底是何意思呢?
boolean enabled 是否可用
boolean accountNonExpired 账户是否失效
boolean credentialsNonExpired 秘密是否失效
boolean accountNonLocked 账户是否锁定
2、判断认证用户的状态
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    SysUser sysUser = userDao.findByName(username);
    if(sysUser==null){
        //若用户名不对,直接返回null,表示认证失败。
        return null;
    }
    List authorities = new ArrayList<>();
    List roles = sysUser.getRoles();
    for (SysRole role : roles) {
        authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
    }
    //最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证。
    return new User(sysUser.getUsername(),
                    sysUser.getPassword(),
                    sysUser.getStatus()==1,   // 此刻,只有用户状态为1的用户才能成功通过认证!
                    true,
                    true,
                    true, authorities);
}
5.5 remember me
1、记住我功能原理分析
public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
    public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        // 判断是否勾选记住我
        // 注意:这里this.parameter点进去是上面的private String parameter = "remember-me";
        if (!this.rememberMeRequested(request, this.parameter)) {
            this.logger.debug("Remember-me login not requested.");
        } else {
            //若勾选就调用onLoginSuccess方法
            this.onLoginSuccess(request, response, successfulAuthentication);
        }
    }
}
再点进去上面if判断中的rememberMeRequested方法,还在当前类中:
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
    if (this.alwaysRemember) {
        return true;
    } else {
        // 从上面的字parameter的值为"remember-me"
        // 也就是说,此功能提交的属性名必须为"remember-me"
        String paramValue = request.getParameter(parameter);
        // 这里我们看到属性值可以为:true,on,yes,1。
        if (paramValue != null && (paramValue.equalsIgnoreCase("true") ||
                                   paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") ||
                                   paramValue.equals("1"))) {
            //满足上面条件才能返回true
            return true;
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Did not send remember-me cookie (principal did not set
                                  parameter '" + parameter + "')");
            }
            return false;
        }
    }
}
如果上面方法返回true,就表示页面勾选了记住我选项了。
继续顺着调用的方法找到PersistentTokenbasedRememberMeServices的onLoginSuccess方法:
public class PersistentTokenbasedRememberMeServices extends AbstractRememberMeServices {
    protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
                                  Authentication successfulAuthentication) {
        // 获取用户名
        String username = successfulAuthentication.getName();
        this.logger.debug("Creating new persistent login for user " + username);
        //创建记住我的token
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username,
                                                                                  this.generateSeriesData(), this.generateTokenData(), new Date());
        try {
            //将token持久化到数据库
            this.tokenRepository.createNewToken(persistentToken);
            //将token写入到浏览器的cookie中
            this.addcookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }
    }
}
2、开启remember me过滤器

    
    
    

3、持久化remember me信息
创建一张表,注意这张表的名称和字段都是固定的,不要修改
CREATE TABLE `persistent_logins` (
    `username` varchar(64) NOT NULL,
    `series` varchar(64) NOT NULL,
    `token` varchar(64) NOT NULL,
    `last_used` timestamp NOT NULL,
    PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
然后将spring-security.xml中 改为:

5.6 授权准备工作
1、为了模拟授权操作,咱们临时编写两个业务功能:
//ProductController
@Controller
@RequestMapping("/product")
public class ProductController {
    @RequestMapping("/findAll")
    public String findAll(){
        return "product-list";
    }
}
//OrderController
@Controller
@RequestMapping("/order")
public class OrderController {
    @RequestMapping("/findAll")
    public String findAll(){
        return "order-list";
    }
}

2、授权操作
说明:SpringSecurity可以通过注解的方式来控制类或者方法的访问权限。注解需要对应的注解支持,若注解放在controller类中,对应注解支持应该放在mvc配置文件中,因为controller类是有mvc配置文件扫描并创建的,同理,注解放在service类中,对应注解支持应该放在spring配置文件中。由于我们现在是模拟业务操作,并没有service业务代码,所以就把注解放在controller类中了。
3、开启授权的注解支持

4、在注解支持对应类或者方法上添加注解
//表示当前类中所有方法都需要ROLE_ADMIN或者ROLE_PRODUCT才能访问
@Controller
@RequestMapping("/product")
@RolesAllowed({"ROLE_ADMIN","ROLE_PRODUCT"})//JSR-250注解
public class ProductController {
    @RequestMapping("/findAll")
    public String findAll(){
        return "product-list";
    }
}
//表示当前类中findAll方法需要ROLE_ADMIN或者ROLE_PRODUCT才能访问
@Controller
@RequestMapping("/product")
public class ProductController {
    @RequestMapping("/findAll")
    @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_PRODUCT')")//spring表达式注解
    public String findAll(){
        return "product-list";
    }
}
//表示当前类中所有方法都需要ROLE_ADMIN或者ROLE_PRODUCT才能访问
@Controller
@RequestMapping("/product")
@Secured({"ROLE_ADMIN","ROLE_PRODUCT"})//SpringSecurity注解
public class ProductController {
    @RequestMapping("/findAll")
    public String findAll(){
        return "product-list";
    }
}
六、JWT 6.1 概念说明
从分布式认证流程中,我们不难发现,这中间起最关键作用的就是token,token的安全与否,直接关系到系统的健壮性,这里我们选择使用JWT来实现token的生成和校验。
JWT,全称JSON Web Token,官网地址https://jwt.io,是一款出色的分布式身份校验方案。可以生成token,也可以解析检验token。

JWT生成的token由三部分组成:
头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。
载荷:token中存放有效信息的部分,比如用户名,用户角色,过期时间等,但是不要放密码,会泄露!
签名:将头部与载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。
6.2 非对称加密RSA
基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
私钥加密,持有私钥或公钥才可以解密
公钥加密,持有私钥才可解密
优点:安全,难以破解
缺点:算法比较耗时,为了安全,可以接受
历史:三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA。
1、JWT相关工具类

    io.jsonwebtoken
    jjwt-api
    0.10.7


    io.jsonwebtoken
    jjwt-impl
    0.10.7
    runtime


    io.jsonwebtoken
    jjwt-jackson
    0.10.7
    runtime

2、载荷对象
@Data
public class Payload {
    private String id;
    private T userInfo;
    private Date expiration;
}
3、JWT工具类
public class JwtUtils {
    private static final String JWT_PAYLOAD_USER_KEY = "user";
    
    public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey,
                                                      int expire) {
        return Jwts.builder()
            .claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
            .setId(createJTI())
            .setExpiration(DateTime.now().plusMinutes(expire).toDate())
            .signWith(privateKey, SignatureAlgorithm.RS256)
            .compact();
    }
    
    public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey,
                                                      int expire) {
        return Jwts.builder()
            .claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
            .setId(createJTI())
            .setExpiration(DateTime.now().plusSeconds(expire).toDate())
            .signWith(privateKey, SignatureAlgorithm.RS256)
            .compact();
    }
    
    private static Jws parserToken(String token, PublicKey publicKey) {
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }
    private static String createJTI() {
        return new String(base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
    }
    
    public static  Payload getInfoFromToken(String token, PublicKey publicKey, Class
                                                  userType) {
        Jws claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload claims = new Payload<>();
        claims.setId(body.getId());
        claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(),
                                            userType));
        claims.setExpiration(body.getExpiration());
        return claims;
    }
    
    public static  Payload getInfoFromToken(String token, PublicKey publicKey) {
        Jws claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload claims = new Payload<>();
        claims.setId(body.getId());
        claims.setExpiration(body.getExpiration());
        return claims;
    }
}
4、RSA工具类
public class RsaUtils {
    private static final int DEFAULT_KEY_SIZE = 2048;
    
    public static PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }
    
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }
    
    private static PublicKey getPublicKey(byte[] bytes) throws Exception {
        bytes = base64.getDecoder().decode(bytes);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }
    
    private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException,
    InvalidKeySpecException {
        bytes = base64.getDecoder().decode(bytes);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }
    
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String
                                   secret, int keySize) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        publicKeyBytes = base64.getEncoder().encode(publicKeyBytes);
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        privateKeyBytes = base64.getEncoder().encode(privateKeyBytes);
        writeFile(privateKeyFilename, privateKeyBytes);
    }
    private static byte[] readFile(String fileName) throws Exception {
        return Files.readAllBytes(new File(fileName).toPath());
    }
    private static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }
}
6.3 分布式认证思路
回顾集中式认证流程
用户认证:
使用UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法实现认证功能,该过滤器父类中successfulAuthentication方法实现认证成功后的操作。

身份校验:
使用BasicAuthenticationFilter过滤器中doFilterInternal方法验证是否登录,以决定能否进入后续过滤器
分析分布式认证流程
用户认证:
由于,分布式项目,多数是前后端分离的架构设计,我们要满足可以接受异步post的认证请求参数,需要修改UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法,让其能够接收请求体。
另外,默认successfulAuthentication方法在认证通过后,是把用户信息直接放入session就完事了,现在我们需要修改这个方法,在认证通过后生成token并返回给用户。

身份校验:
原来BasicAuthenticationFilter过滤器中doFilterInternal方法校验用户是否登录,就是看session中是否有用户信息,我们要修改为,验证用户携带的token是否合法,并解析出用户信息,交给SpringSecurity,以便于后续的授权功能可以正常使用。
6.4 分布式认证实现
1、通用模块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fLrPP17B-1648283430507)(https://gitee.com/qingxi5/typora/raw/master/note/20210629152137.png)]

在通用子模块中编写测试类生成rsa公钥和私钥
public class RsaUtilsTest {
    private String publicFile = "D:\auth_key\rsa_key.pub";
    private String privateFile = "D:\auth_key\rsa_key";
    @Test
    public void generateKey() throws Exception {
        RsaUtils.generateKey(publicFile, privateFile, "heima", 2048);
    }
}
2、认证服务
2.1 创建认证服务配置文件
server:
port: 9001
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///security_authority
username: root
password: root
mybatis:
type-aliases-package: com.yxj.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.yxj: debug
heima:
key:
pubKeyPath: D:\auth_key\rsa_key.pub
priKeyPath: D:\auth_key\rsa_key
2.2 提供解析公钥和私钥的配置类
@Data
@ConfigurationProperties(prefix = "heima.key")
public class RsaKeyProperties {
    private String pubKeyPath;
    private String priKeyPath;
    private PublicKey publicKey;
    private PrivateKey privateKey;
    @PostConstruct
    public void loadKey() throws Exception {
        publicKey = RsaUtils.getPublicKey(pubKeyPath);
        privateKey = RsaUtils.getPrivateKey(priKeyPath);
    }
}
2.3、创建认证服务启动类
@SpringBootApplication
@MapperScan("com.yxj.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class)
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }
}
2.4、编写认证过滤器
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;
    private RsaKeyProperties prop;
    public TokenLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop)
    {
        this.authenticationManager = authenticationManager;
        this.prop = prop;
    }
    
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
    {
        try {
            //将json格式请求体转成JavaBean对象
            SysUser user = new ObjectMapper().readValue(req.getInputStream(), SysUser.class);
            return authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    user.getUsername(),
                    user.getPassword())
            );
        } catch (Exception e) {
            try {
                //如果认证失败,提供自定义json格式异常
                res.setContentType("application/json;charset=utf-8");
                res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                PrintWriter out = res.getWriter();
                Map map = new HashMap();
                map.put("code", HttpServletResponse.SC_UNAUTHORIZED);
                map.put("message", "账号或密码错误!");
                out.write(new ObjectMapper().writevalueAsString(map));
                out.flush();
                out.close();
            } catch (Exception e1) {
                e1.printStackTrace();
            }
            throw new RuntimeException(e);
        }
    }
    
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,FilterChain chain, Authentication auth) {
        //得到当前认证的用户对象
        SysUser user = new SysUser();
        user.setUsername(auth.getName());
        user.setRoles((List) auth.getAuthorities());
        //json web token构建
        String token = JwtUtils.generateTokenExpireInMinutes(user, prop.getPrivateKey(), 24*60);
        //返回token
        res.addHeader("Authorization", "Bearer " + token);
        try {
            //登录成功時,返回json格式进行提示
            res.setContentType("application/json;charset=utf-8");
            res.setStatus(HttpServletResponse.SC_OK);
            PrintWriter out = res.getWriter();
            Map map = new HashMap();
            map.put("code", HttpServletResponse.SC_OK);
            map.put("message", "登陆成功!");
            out.write(new ObjectMapper().writevalueAsString(map));
            out.flush();
            out.close();
        } catch (Exception e1) {
            e1.printStackTrace();
        }
    }
}
2.5 编写检验token过滤器
public class TokenVerifyFilter extends BasicAuthenticationFilter {
    private RsaKeyProperties prop;
    public TokenVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop)
    {
        super(authenticationManager);
        this.prop = prop;
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain chain) {
        try {
            //请求体的头中是否包含Authorization
            String header = request.getHeader("Authorization");
            //Authorization中是否包含Bearer,不包含直接返回
            if (header == null || !header.startsWith("Bearer ")) {
                chain.doFilter(request, response);
                responseJson(response);
                return;
            }
            //获取权限失败,会抛出异常
            UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
            //获取后,将Authentication写入SecurityContextHolder中供后序使用
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } catch (Exception e) {
            responseJson(response);
            e.printStackTrace();
        }
    }
    
    private void responseJson(HttpServletResponse response) {
        try {
            //未登录提示
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter out = response.getWriter();
            Map map = new HashMap();
            map.put("code", HttpServletResponse.SC_FORBIDDEN);
            map.put("message", "请登录!");
            out.write(new ObjectMapper().writevalueAsString(map));
            out.flush();
            out.close();
        } catch (Exception e1) {
            e1.printStackTrace();
        }
    }
    
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        if (token != null) {
            //通过token解析出载荷信息
            Payload payload = JwtUtils.getInfoFromToken(token.replace("Bearer ", ""),
                                                                 prop.getPublicKey(), SysUser.class);
            SysUser user = payload.getUserInfo();
            //不为null,返回
            if (user != null) {
                return new UsernamePasswordAuthenticationToken(user, null, user.getRoles());
            }
            return null;
        }
        return null;
    }
}
2.6 编写SpringSecurity配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService myCustomUserService;
    @Autowired
    private RsaKeyProperties prop;
    @Bean
    public BCryptPasswordEncoder myPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            //关闭跨站请求防护
            .cors().and().csrf().disable()
            //允许不登陆就可以访问的方法,多个用逗号分隔
            .authorizeRequests().antMatchers("/product").hasAnyRole("USER")
            //其他的需要授权后访问
            .anyRequest().authenticated()
            .and()
            //增加自定义认证过滤器
            .addFilter(new TokenLoginFilter(authenticationManager(), prop))
            //增加自定义验证认证过滤器
            .addFilter(new TokenVerifyFilter(authenticationManager(), prop))
            // 前后端分离是无状态的,不用session了,直接禁用。
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        //UserDetailsService类
        auth.userDetailsService(myCustomUserService)
            //加密策略
            .passwordEncoder(myPasswordEncoder());
    }
}
3、资源服务
资源服务中只能通过公钥验证认证。不能签发token
3.1 编写产品服务配置文件,切记这里只能有公钥地址!
server:
port: 9002
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///security_authority
username: root
password: root
mybatis:
type-aliases-package: com.yxj.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.yxj: debug
heima:
key:
pubKeyPath: D:\auth_key\rsa_key.pub
3.2 编写读取公钥的配置类
@Data
@ConfigurationProperties(prefix = "heima.key")
public class RsaKeyProperties {
    private String pubKeyPath;
    private PublicKey publicKey;
    @PostConstruct
    public void loadKey() throws Exception {
        publicKey = RsaUtils.getPublicKey(pubKeyPath);
    }
}
3.3 编写启动类
@SpringBootApplication
@MapperScan("com.yxj.mapper")
@EnableConfigurationProperties(RsaKeyProperties.class)
public class ProductApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductApplication.class, args);
    }
}
3.4 复制认证服务中SpringSecurity配置类做修改,去掉“增加自定义认证过滤器”即可!
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService myCustomUserService;
    @Autowired
    private RsaKeyProperties prop;
    @Bean
    public BCryptPasswordEncoder myPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            //关闭跨站请求防护
            .cors().and().csrf().disable()
            //允许不登陆就可以访问的方法,多个用逗号分隔
            .authorizeRequests().antMatchers("/product").hasAnyRole("USER")
            //其他的需要授权后访问
            .anyRequest().authenticated()
            .and()
            //增加自定义验证认证过滤器
            .addFilter(new TokenVerifyFilter(authenticationManager(), prop))
            // 前后端分离是无状态的,不用session了,直接禁用。
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        //UserDetailsService类
        auth.userDetailsService(myCustomUserService)
            //加密策略
            .passwordEncoder(myPasswordEncoder());
    }
}
3.5 编写产品处理器
@RestController
@RequestMapping("/product")
public class ProductController {
    @GetMapping
    public String findAll(){
        return "产品测试成功!";
    }
}
七、OAuth2.0 7.1 概念说明
先说OAuth,OAuth是Open Authorization的简写。
OAuth协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAuth的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAuth是安全的。
OAuth2.0是OAuth协议的延续版本,但不向前兼容(即完全废止了OAuth1.0)。
使用场景
假设,A网站是一个打印照片的网站,B网站是一个存储照片的网站,二者原本毫无关联。
如果一个用户想使用A网站打印自己存储在B网站的照片,那么A网站就需要使用B网站的照片资源才行。
按照传统的思考模式,我们需要A网站具有登录B网站的用户名和密码才行,但是,现在有了OAuth2,只需要A网站获取到使用B网站照片资源的一个通行令牌即可!这个令牌无需具备操作B网站所有资源的权限,也无需永久有效,只要满足A网站打印照片需求即可。
这么听来,是不是有点像单点登录?NONONO!千万不要混淆概念!单点登录是用户一次登录,自己可以操作其他关联的服务资源。OAuth2则是用户给一个系统授权,可以直接操作其他系统资源的一种方式。
但SpringSecurity的OAuth2也是可以实现单点登录的!
总结一句:SpringSecurity的OAuth2可以做服务之间资源共享,也可以实现单点登录!
7.2 四种授权方式
1、授权码模式(authorization code)

流程
说明:【A服务客户端】需要用到【B服务资源服务】中的资源
第一步:【A服务客户端】将用户自动导航到【B服务认证服务】,这一步用户需要提供一个回调地址,以备【B服务认证服务】返回授权码使用。
第二步:用户点击授权按钮表示让【A服务客户端】使用【B服务资源服务】,这一步需要用户登录B服务,也就是说用户要事先具有B服务的使用权限。
第三步:【B服务认证服务】生成授权码,授权码将通过第一步提供的回调地址,返回给【A服务客户端】。注意这个授权码并非通行【B服务资源服务】的通行凭证。
第四步:【A服务认证服务】携带上一步得到的授权码向【B服务认证服务】发送请求,获取通行凭证token。
第五步:【B服务认证服务】给【A服务认证服务】返回令牌token和更新令牌refresh token。

使用场景
授权码模式是OAuth2中最安全最完善的一种模式,应用场景最广泛,可以实现服务之间的调用,常见的微信,QQ等第三方登录也可采用这种方式实现。
2、简化模式(implicit)

流程
说明:简化模式中没有【A服务认证服务】这一部分,全部有【A服务客户端】与B服务交互,整个过程不再有
授权码,token直接暴露在浏览器。
第一步:【A服务客户端】将用户自动导航到【B服务认证服务】,这一步用户需要提供一个回调地址,以备
【B服务认证服务】返回token使用,还会携带一个【A服务客户端】的状态标识state。
第二步:用户点击授权按钮表示让【A服务客户端】使用【B服务资源服务】,这一步需要用户登录B服务,也
就是说用户要事先具有B服务的使用权限。
第三步:【B服务认证服务】生成通行令牌token,token将通过第一步提供的回调地址,返回给【A服务客户
端】。

使用场景
适用于A服务没有服务器的情况。比如:纯手机小程序,Javascript语言实现的网页插件等。
3、密码模式(resource owner password credentials)

流程
第一步:直接告诉【A服务客户端】自己的【B服务认证服务】的用户名和密码
第二步:【A服务客户端】携带【B服务认证服务】的用户名和密码向【B服务认证服务】发起请求获取
token。
第三步:【B服务认证服务】给【A服务客户端】颁发token。

使用场景
此种模式虽然简单,但是用户将B服务的用户名和密码暴露给了A服务,需要两个服务信任度非常高才能使用。
4、客户端模式(client credentials)

流程
说明:这种模式其实已经不太属于OAuth2的范畴了。A服务完全脱离用户,以自己的身份去向B服务索取
token。换言之,用户无需具备B服务的使用权也可以。完全是A服务与B服务内部的交互,与用户无关了。
第一步:A服务向B服务索取token。
第二步:B服务返回token给A服务。

使用场景
A服务本身需要B服务资源,与用户无关。
7.3 实战案例
1、创建资源模块
提供配置文件
server:
	port: 9002
	spring:
	datasource:
	driver-class-name: com.mysql.jdbc.Driver
	url: jdbc:mysql:///security_authority
	username: root
	password: root
main:
	allow-bean-definition-overriding: true
mybatis:
	type-aliases-package: com.yxj.domain
	configuration:
		map-underscore-to-camel-case: true
logging:
	level:
		com.yxj: debug
提供启动类
@SpringBootApplication
@MapperScan("com.yxj.mapper")
public class OAuthSourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OAuthSourceApplication.class, args);
    }
}
提供处理器
@RestController
@RequestMapping("/product")
public class ProductController {
    @GetMapping
    public String findAll(){
        return "查询产品列表成功!";
    }
}
编写资源管理配置类
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true)
public class OauthSourceConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    private DataSource dataSource;
    
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }
    @Override
    public void configure(ResourceServerSecurityConfigurer resources)throws Exception{
        TokenStore tokenStore = new JdbcTokenStore(dataSource);
        resources.resourceId("product_api")//指定当前资源的id,非常重要!必须写!
            .tokenStore(tokenStore);//指定保存token的方式
    }
    @Override
    public void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests()
            //指定不同请求方式访问资源所需要的权限,一般查询是read,其余是write。
            .antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
            .antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
            .antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
            .antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
            .antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
            .and()
            .headers().addHeaderWriter((request, response) -> {
            response.addHeader("Access-Control-Allow-Origin", "*");//允许跨域
            if (request.getMethod().equals("OPTIONS")) {//如果是跨域的预检请求,则原封不动向下传达请
                求头信息
                    response.setHeader("Access-Control-Allow-Methods", request.getHeader("AccessControl-Request-Method"));
                response.setHeader("Access-Control-Allow-Headers", request.getHeader("AccessControl-Request-Headers"));
            }
        });
    }
}
2、创建授权模块
提供配置文件
server:
port: 9001
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///security_authority
username: root
password: root
main:
allow-bean-definition-overriding: true # 这个表示允许我们覆盖OAuth2放在容器中的bean对象,一定要
配置
mybatis:
type-aliases-package: com.yxj.domain
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.yxj: debug
提供启动类
@SpringBootApplication
@MapperScan("com.yxj.mapper")
public class OauthServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(OauthServerApplication.class, args);
    }
}
提供SpringSecurity配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService myCustomUserService;
    @Bean
    public BCryptPasswordEncoder myPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            //所有资源必须授权后访问
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginProcessingUrl("/login")
            .permitAll()//指定认证页面可以匿名访问
            //关闭跨站请求防护
            .and().csrf().disable();
    }
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        //UserDetailsService类
        auth.userDetailsService(myCustomUserService)
            //加密策略
            .passwordEncoder(myPasswordEncoder());
    }
    //AuthenticationManager对象在OAuth2认证服务中要使用,提取放入IOC容器中
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
提供OAuth2授权配置类
@Configuration
@EnableAuthorizationServer
public class OauthServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private DataSource dataSource;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;
    //从数据库中查询出客户端信息
    @Bean
    public JdbcClientDetailsService clientDetailsService() {
        return new JdbcClientDetailsService(dataSource);
    }
    //token保存策略
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }
    //授权信息保存策略
    @Bean
    public ApprovalStore approvalStore() {
        return new JdbcApprovalStore(dataSource);
    }
    //授权码模式专用对象
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new JdbcAuthorizationCodeServices(dataSource);
    }
    //指定客户端登录信息来源
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService());
    }
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.allowFormAuthenticationForClients();
        oauthServer.checkTokenAccess("isAuthenticated()");
    }
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
            .approvalStore(approvalStore())
            .authenticationManager(authenticationManager)
            .authorizationCodeServices(authorizationCodeServices())
            .tokenStore(tokenStore());
    }
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/780944.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号