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

SpringSecurity单体应用

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

SpringSecurity单体应用

SpringSecurity单体应用

住:本文讲述的是Security在单体架构的应用,不支持集群跨域。另外,本文基于前后端不分离,使用的前端模板引擎是Thymeleaf。

一、导入Security依赖

第一个依赖是SpringBoot为Security提供的starter依赖,导入后,Security立即生效,会默认生成一个用户名和密码(项目重启后控制台可见),使项目中所有的请求都需要认证。
第二个依赖是thymeleaf模板引擎未支持Security提供的依赖,这个依赖其实不是必须的,下文会简单提一下它的用法。


   org.springframework.boot
    spring-boot-starter-security


    org.thymeleaf.extras
    thymeleaf-extras-springsecurity5

二、创建UserDetails对象

一般我们的项目中已经有了User对象,直接让它实现UserDetails接口即可。UserDetails是Security默认的用户信息存储媒介,它只存储用户名(username)、密码(password)、权限(authorities)和其他一些用户状态,具体如下:

public interface UserDetails extends Serializable {
	// 获取授予用户的权限,包括权限级别authority和级别角色role
    Collection getAuthorities();
	// 获取用户密码
    String getPassword();
    // 获取用户名
    String getUsername();
	// 标识用户的帐户是否已过期,true(未过期)、false(已过期),过期帐户无法验证。
    boolean isAccountNonExpired();
	// 标识用户是被锁定还是未锁定,true(未锁定),false(锁定),锁定的用户无法进行身份验证。
    boolean isAccountNonLocked();
	// 标识用户的凭据(密码)是否已过期,true(未过期)、false(已过期),过期的凭据会阻止身份验证。
    boolean isCredentialsNonExpired();
	// 标识用户是启用还是禁用,true(启用),false(禁用),禁用的用户无法进行身份验证。
    boolean isEnabled();
}

通常来说,我们会在自己的User对象中创建authority(权限级别)、role(级别角色)两个属性,用于实现getAuthorities()。当然,如果你的项目很简单,不需要级别角色的定义,只创建authority属性也是可以的。
username和password相信不用我多说了,我们自己的User对象就有这两个属性,它们的get方法就是UserDetails的接口方法。
至于四个boolean类型的接口方法,如果项目中需要这些功能,就相应添加boolean类型的accountNonExpired、accountNonLocked、credentialsNonExpired、enabled属性。如果项目中没有使用的必要,就直接实现这四个方法全部设定为true即可。

User对象实现UserDetails
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements UserDetails {
    private Integer id;
    // 此处,用户名为email,并不是一定要取名username、password、authority、role,实现UserDetails接口方法时区分好这些字段即可。
    // 如果你的User对象还有其他额外的字段,对UserDetails的实现是完全没有影响的,保持它们的原样即可。
    private String email;
    private String password;
    // 注意,此处的Authority和Role是我定义的枚举类,这也是权限字段常用的定义方式。
    private Authority authority;
    private Role role;
   	
    // 账号权限
    @Override
    public Collection getAuthorities() {
    	// 此处注意,因为UserDetails把authority和role都放入了authoritie属性中,所以Security规定role前加上级别角色标识符“ROLE_”,以便区分authoritie列表中哪些元素是authority,哪些是role。
    	// 当然,你也可以把级别角色标识符“ROLE_”定义在枚举类属性中,此处就可以直接传参role.toString()了。
        return AuthorityUtils.createAuthorityList(authority.toString(), "ROLE_" + role.toString());
    }
    // 账号名
    @Override
    public String getUsername() {
    	// 因为我们的用户名为email,所以需要额外实现getUsername()方法
    	// 而getPassword()方法不用实现,因为@Data注解已经帮我们实现
        return email;
    }
    // 账号没有过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    // 账号没有锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    // 凭证未过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    // 账号可用
    @Override
    public boolean isEnabled() {
        return true;
    }
}
Authority枚举类
import com.alibaba.fastjson.annotation.JSONType;

@JSONType(serializeEnumAsJavaBean = true)
public enum Authority implements baseEnum {
    MEMBER(1, "普通成员"),
    ADMIN(2, "普通管理员"),
    SUPER(3, "超级管理员");

    private Integer code;
    private String name;
    Authority(Integer code, String name){
        this.code = code;
        this.name = name;
    }

    @Override
    public Integer getCode() {
        return code;
    }

    @Override
    public String getName() {
        return name;
    }

    public static Authority getEnum(Integer code){
        for (Authority value : Authority.values()) {
            if(value.getCode().equals(code)){
                return value;
            }
        }
        return null;
    }
}
Role枚举类
import com.alibaba.fastjson.annotation.JSONType;

@JSONType(serializeEnumAsJavaBean = true)
public enum Role implements baseEnum {
	// 如果在枚举类中直接适应Security,直接定义为ROLE_AD、ROLE_HR、ROLE_MD、ROLE_TD即可。
    AD(1, "行政部成员"),
    HR(2, "人力资源部成员"),
    MD(3, "市场部成员"),
    TD(3, "技术部成员");

    private Integer code;
    private String name;
    Role(Integer code, String name){
        this.code = code;
        this.name = name;
    }

    @Override
    public Integer getCode() {
        return code;
    }

    @Override
    public String getName() {
        return name;
    }

    public static Role getEnum(Integer code){
        for (Role value : Role.values()) {
            if(value.getCode().equals(code)){
                return value;
            }
        }
        return null;
    }
}

关于枚举类的使用可查看我的上一篇文章,枚举类通用接口baseEnum有讲到它的来源与使用。

三、创建UserDetailsService对象

同样的,我们的项目中已经有了UserServiceImpl,直接让它再实现UserDetailsService接口即可。UserDetailsService是Security默认的用户信息查询接口,里面只有一个接口方法,如下:

public interface UserDetailsService {
	// 参数var1代表用户名,即根据用户名查询UserDetails对象,而我们使用User对象继承了UserDetails,所以相当于查询User对象。
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
UserServiceImpl实现UserDetailsService
@Service
public class UserServiceImpl implements UserService, UserDetailsService {
    @Resource
    private UserDao userDao;
	
	// 同样的,UserServiceImpl中还有大量实现我们自定义的UserService的方法,并不影响对UserDetailsService的实现
	@Override
	public User selectByEmail(String emails){
		return userDao.selectByEmail(email);
	}
    
	@Override
	public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
		// 因为我们的用户名是email,所以就是用email去查询User对象啦
        return this.selectByEmail(email);
    }
}

关于UserService和UserDao的实现,这里就不用细说了吧!
UserServiceImpl之所以要把selectByEmail()方法单独列出来,是因为loadUserByUsername()方法返回的时UserDetails对象,这个对象只包含了authorities、username、password等属性,这是Security想要的,但不是我们想要的,我们想要的是完整的User对象,selectByEmail返回的正是User对象,下文会讲到它的使用。

四、配置DataSource

这一步就很常规了,既然都涉及到了查询数据库的用户信息,那么,对于数据源的配置当然是不可少的。

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
五、创建SecurityConfig配置类

注意,这一步信息量可就大了,代码的每一行注释都值得仔细阅读。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter implements ProjectConstant {
    @Autowired
    private UserService userService;
    @Autowired
    private DataSource dataSource;
    // 自定义登录失败处理器,下文会给出
    @Autowired
    private MyFailureHandler failureHandler;
    // 自定义登录验证码过滤器,下文会给出
    @Autowired
    private CaptchaFilter captchaFilter;
	
	// 封装PersistentTokenRepository对象,用于辅助实现基于cookie和数据库的remember-me记住我功能,下文会讲到。
    @Bean
    PersistentTokenRepository tokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        return tokenRepository;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
    	// 设置Security忽略静态资源的访问拦截
        web.ignoring().antMatchers("/static
        request.setAttribute(ATTRIBUTE_MESSAGE, R.error(exception.getMessage()));
        request.getRequestDispatcher("/login").forward(request, response);
    }
}
自定义验证码过滤器CaptchaFilter
@Component
public class CaptchaFilter extends OncePerRequestFilter implements ProjectConstant {

    @Autowired
    private MyFailureHandler failureHandler;
    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 判断是否登录请求,登录请求才检查验证码
        if((request.getContextPath()+"/login").equals(request.getRequestURI()) && "POST".equals(request.getMethod())){
            
            try {
                validateCaptcha(request);
            }catch (CaptchaException e){
                failureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
            filterChain.doFilter(request, response);
        }
        filterChain.doFilter(request, response);
    }

    private void validateCaptcha(HttpServletRequest request) {
    	// 此处自己规定前端验证码参数必须为”captcha“
        String captcha = request.getParameter(”captcha“);
        if(StringUtils.isBlank(captcha)){
            throw new CaptchaException("验证码不能为空!");
        }
        String captchaKey = RedisKeyUtils.getCaptchaKey(request.getRemoteAddr());
        Object captchaRedis = redisTemplate.opsForValue().get(captchaKey);
        if(ObjectUtils.isEmpty(captchaRedis)){
            throw new CaptchaException("验证码已失效,请刷新!");
        }
        String captchaLogin = (String) captchaRedis;
        if(!captcha.toUpperCase().equals(captchaLogin)){
            throw new CaptchaException("验证码错误!");
        }
    }
}

此处,验证码文本我是存储在Redis中的,用request.getRemoteAddr()获取客户端IP地址作为验证码的拥有者。初学者可以存储在Session中,Session对每个用户都是独立空间,不用额外指定验证码的所有者。

Controller层的配合

Security默认会生成一个简陋的登录页,只能输入用户名和密码,没有remember-me复选框,更没有图形验证码,所以自定义我们自己的登录页肯定是必须的。这就需要Controller层的SpringMVC方法配合。

// return "login"就是返回我们自定义的登录视图页面了,这个前端页面就不用我给出来了吧!
// 这个MCV方法之所以同时绑定了POST请求,是因为我在MyFailureHandler代码注释中讲到的善意的欺骗,算是Security使用中的一个坑吧,以这种巧妙的方式解决了。
@RequestMapping(value = "/login", method = {RequestMethod.POST, RequestMethod.GET})
public String login(){
    return "login";
}
引申

提到Controller层,其实Security也支持在Controller层以注解的方式绑定请求路径和用户权限的关系。常用的注解有@Secured和@PreAuthorize。

// 表示只有技术部成员才能访问的功能
@Secured("ROLE_TD")
@ResponseBody
@GetMapping("/member/technolog")
public String teacher(){
	// 业务代码略
    return JSON.toJSONString(R.ok());
}

// 表示只有普通管理员中的技术部成员才能访问的功能
// 之前在SecurityConfig中写的hasAuthority与hasRole配合使用的例子我不知道对不对,但用注解@PreAuthorize的这个写法我确定是对的
@PreAuthorize("hasAuthority('ADMIN') && hasRole('ROLE_TD')")
@ResponseBody
@GetMapping("/admin/technolog")
public String manager(){
    // 业务代码略
    return JSON.toJSONString(R.ok());
}
前端Thymeleaf模板配合

SpringSecurity开启拦截csrf网络攻击的功能后,会默认在前端所有的form表单增加一个隐藏框,用于识别当前访问环境的安全性。如下:


那么,表单请求的确是自动携带了_csrf值,异步请求怎么办呢,异步请求不会自动携带_csrf值,默认会被Security拦截。这时候就需要我们手动携带_csrf值了,以AJAX请求为例:
1、在HTML页面指定meta标签传值_csrf.headerName与_csrf.token。



2、在Javascript脚本中指定JAX请求携带CSRF令牌

$(document).ajaxSend(function (e, xhr, options) {
    var key = $("meta[name='_csrf_header']").attr("content");
    var value = $("meta[name='_csrf']").attr("content");
    xhr.setRequestHeader(key, value);
});

由此,AJAX异步请求就可以正常访问了。

引申

之前,我们遗留了一个问题,就是Thymeleaf为支持Security提供的依赖有什么作用?这里既然讲到了Thymeleaf,就简单提一下那个依赖的使用。
1、声明此依赖在前端模板中的对象名,就像声明th="http://www.thymeleaf.org"一样。


2、使用sec对象

可以看出,Thymeleaf提供的这个依赖是为了契合Security的配置习惯,用与后端配置类相同的hasAuthority与hasRole语句来绑定前端页面显示与用户权限的关系。
然而,在实际开发中,登录用户的User对象我们肯定会传给前端的,即我们之前缓存在Security的User对象。在所有设计转发视图的SpringMVC方法中,我们都会把User对象绑定到视图中,根据这个User对象依然可以实现判断当前用户权限的目的,进而使用th对象就可以绑定前端页面显示与用户权限的关系,不用引入sec。
1、我们可以在后端创建一个工具类,获取Security缓存的User对象,供业务层代码调用,如下:

@Component
public class SecurityHolder {
    @SneakyThrows
    public User getUser() {
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if(principal instanceof User){
            return (User)principal;
        }
        throw new PrincipalException("SpringSecurity保存的Authentication对象中主要信息Principal无法转换为User对象或者为空");
    }
    
    public String getUsername(){
        return SecurityContextHolder.getContext().getAuthentication().getName();
    }
}

2、SpringMVC方法绑定视图与数据

@GetMapping("/main")
public String main(Model model){
    User user = securityHolder.getUser();
    model.addAttribute(ATTRIBUTE_USER, user);
    return "main";
}

3、Thymeleaf识别User对象做到前端权限隔离

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/573140.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

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

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