Spring Boot与安全

Updated on with 0 views and 0 comments

“认证”(Authentication),是建立一个他声明的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统)。

“授权”(Authorization)指确定一个主体是否允许在你的应用程序执行一个动作的过程。为了抵达需要授权的店,主体的身份已经有认证过程建立。

SpringSecurity与SpringMVC

依赖

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>

配置web.xml

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">
    <display-name>Archetype Created Web Application</display-name>
    <!--Spring Security过滤器链,注意过滤器名称必须叫springSecurityFilterChain-->
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

创建SpringSecurity配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:security="http://www.springframework.org/schema/security"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security.xsd">
    <!--直接释放无需经过SpringSecurity过滤器的静态资源-->
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
<security:http pattern="/failer.jsp" security="none"/>
<security:http pattern="/favicon.ico" security="none"/>
<!--设置可以用spring的el表达式配置Spring Security并自动生成对应配置组件(过滤器)-->
<security:http auto-config="true" use-expressions="true">
<!--指定login.jsp页面可以被匿名访问-->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!--使用spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
<!--指定自定义的认证页面-->
<security:form-login login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp"/>
<!--指定退出登录后跳转的页面-->
<security:logout logout-url="/logout"
logout-success-url="/login.jsp"/>
</security:http>
<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}user"
authorities="ROLE_USER" />
<security:user name="admin" password="{noop}admin"
authorities="ROLE_ADMIN" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>

引入SpringSecurity配置文件到application中

<!--引入SpringSecurity主配置文件-->
<import resource="classpath:spring-security.xml"/>

开启csrf

配置中启动csrf

表单提交时使用隐藏域提交token,这里使用了thymeleaf模板,注意登录,注销都需要提交隐藏域,而且必须是post提交

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>

pringBoot与SpringSecurity

1.引入SpirngSecurity

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2.编写SpringSecurity的配置类

继承WebSecurityConfigurerAdapter

@EnableWebSecurity:配置类带上这个注解,这个注解已经使用了@Configuration,所以不需要再加

控制请求的访问权限

ps:如遇以下错误信息,是模板与springboot冲突的问题,修改一下版本

An attempt was made to call the method org.thymeleaf.spring5.SpringTemplateEngine.setRenderHiddenMarkersBeforeCheckboxes(Z)V but it does not exist. Its class, org.thymeleaf.spring5.SpringTemplateEngine, is available from the following locations:

    jar:file:/E:/springboot/repository/org/thymeleaf/thymeleaf-spring5/3.0.9.RELEASE/thymeleaf-spring5-3.0.9.RELEASE.jar!/org/thymeleaf/spring5/SpringTemplateEngine.class

It was loaded from the following location:

    file:/E:/springboot/repository/org/thymeleaf/thymeleaf-spring5/3.0.9.RELEASE/thymeleaf-spring5-3.0.9.RELEASE.jar


Action:

Correct the classpath of your application so that it contains a single, compatible version of org.thymeleaf.spring5.SpringTemplateEngine
<thymeleaf-spring5.version>3.0.9.RELEASE</thymeleaf-spring5.version>
<!-- 布局功能的支持程序  thymeleaf3主程序  layout2以上版本 -->
<!-- thymeleaf2   layout1-->
<thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>

2.1.x的springboot版本是security5

2.0.x的是security4

spring securuty5 默认使用密码加密,在配置类中创建bean交由springboot管理即可,后续要使用时再注入

@Bean
public PasswordEncoder createPwdEncoder() {
    return new BCryptPasswordEncoder();
}

命名空间使用略有不同,pom文件也要修改一下导入的依赖

<!-- 2.1.x的springboot版本是security5 -->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<!-- 2.0.x的是security4 -->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security4">
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Insert title here</title>
</head>
<body>
<h1 align="center">欢迎光临武林秘籍管理系统</h1>
<div sec:authorize="!isAuthenticated()">
    <h2 align="center">游客您好,如果想查看武林秘籍 <a th:href="@{/userlogin}">请登录</a></h2>
</div>
<div sec:authorize="isAuthenticated()">
    <h2><span sec:authentication="name"></span>,您好,您的角色有:<span sec:authentication="principal.authorities"></span></h2>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="注销">
    </form>
</div>
<hr>
<div sec:authorize="hasRole('VIP1')">
    <h3>普通武功秘籍</h3>
    <ul>
        <li><a th:href="@{/level1/1}">罗汉拳</a></li>
        <li><a th:href="@{/level1/2}">武当长拳</a></li>
        <li><a th:href="@{/level1/3}">全真剑法</a></li>
    </ul>
</div>
<div sec:authorize="hasRole('VIP2')">
    <h3>高级武功秘籍</h3>
    <ul>
        <li><a th:href="@{/level2/1}">太极拳</a></li>
        <li><a th:href="@{/level2/2}">七伤拳</a></li>
        <li><a th:href="@{/level2/3}">梯云纵</a></li>
    </ul>
</div>

<div sec:authorize="hasRole('VIP3')">
    <h3>绝世武功秘籍</h3>
    <ul>
        <li><a th:href="@{/level3/1}">葵花宝典</a></li>
        <li><a th:href="@{/level3/2}">龟派气功</a></li>
        <li><a th:href="@{/level3/3}">独孤九剑</a></li>
    </ul>
</div>


</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h1 align="center">欢迎登陆武林秘籍管理系统</h1>
	<hr>
	<div align="center">
		<form th:action="@{/userlogin}" method="post">
			用户名:<input name="user"/><br>
			密码:<input name="pwd"><br/>
			<input type="checkbox" name="remember">记住我
			<input type="submit" value="登陆">
		</form>
	</div>
</body>
</html>
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        //定制请求的授权规则
        http.authorizeRequests().antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("VIP1")
                .antMatchers("/level2/**").hasRole("VIP2")
                .antMatchers("/level3/**").hasRole("VIP3");
        //开启自动配置的登录功能,如果没有登录,会来到登录页面
        http.formLogin().usernameParameter("user").passwordParameter("pwd").loginPage("/userlogin");    //.loginPage("/userlogin")表示自定义登录页面,不指定就回到默认的
        //1./login来到登录页
        //2.重定向到/login?error表示登录失败
        //3.还有更多详细规定
        //4.默认post形式的/login代表处理登录
        //5.一旦定制loginPage,那么loginPage的post请求就是登录

        //开启自动配置的注销功能
        http.logout().logoutSuccessUrl("/");   //.logoutSuccessUrl("/")表示注销成功返回到首页
        //1.访问/logout 表示用户注销,清空session
        //2.注销成功会返回/login?logout

        //开启记住我功能
        http.rememberMe().rememberMeParameter("remember");
        //登录成功后将cookie发给浏览器保存,以后访问页面会带上这个cookie,只要通过检查就可以免登录
        //点击注销会删除cookie
    }

    //定义认证规则
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        super.configure(auth);
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("wu").password(new BCryptPasswordEncoder().encode("458974")).roles("VIP1","VIP2")
        .and()
        .withUser("ruo").password("458974").roles("VIP3","VIP2");
    }
}

记住我功能

表单中新增复选框,name值为remember-me

<input type="checkbox" name="remember-me" title="记住我" checked>

SpringSecurity配置中开启remember-me功能,设置过期时间和UserDetailService

.and()
.rememberMe().tokenValiditySeconds(86400)
.userDetailsService(myUserDetailService);

这样之后还是存在安全问题,别人可以拿着cookie来进行访问,我们需要持久化cookie

创建持久化表,这是SpringSecurity官方提供的表

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

开启注解权限支持

一共有三种注解支持,ka

xml形式

<!--
开启权限控制注解支持
jsr250-annotations="enabled"表示支持jsr250-api的注解,需要jsr250-api的jar包
pre-post-annotations="enabled"表示支持spring表达式注解
secured-annotations="enabled"这才是SpringSecurity提供的注解
-->
<security:global-method-security jsr250-annotations="enabled"
pre-post-annotations="enabled"
secured-annotations="enabled"/>

springboot配置类形式,在SpringSecurity配置类上添加注解

@EnableGlobalMethodSecurity(prePostEnabled = true,jsr250Enabled = true,securedEnabled = true)    //开权限方法权限注解支持

在controller或者service上开启权限控制

//@PreAuthorize("hasRole('ROLE_MANAGER')")
@Secured({"ROLE_MANAGER","ROLE_USER"})
@RolesAllowed({"ROLE_USER","ROLE_MANAGER"})

从数据库中验证信息并进行简单的权限验证

POM依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

自定义UserDetail

@Data
public class User implements UserDetails {

    private Integer id;

    private String username;

    private String password;

    private Long gmtCreate;

    private Long gmtModified;

    private String avatarUrl;

    private Integer vipLevel;

    private String vipName;

    private Boolean status;

    private String name;

    private Integer sex;

    private String description;

    private List<Role> roleList = new ArrayList();

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roleList;
    }
}

自定义GrantedAuthority

@Data
public class Role implements GrantedAuthority {
    private Integer id;

    private String name;

    private String description;

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

实现UserDetailsService接口

重新写loadUserByUsername方法

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserExample example = new UserExample();
        example.createCriteria().andUsernameEqualTo(username);
        com.wu.manager.pojo.User user = userMapper.selectByExample(example).get(0);
        user.getVipLevel();
        UserGrade userGrade = userGradeMapper.selectByPrimaryKey(user.getVipLevel());
        user.setVipName(userGrade.getGradeName());
        if (user == null) return null;
        List<Role> authorities = authorities(user.getId());
        user.setRoleList(authorities);
        return user;
    }

    //给当前用户指定角色
    private List<Role> authorities(Integer id) {
        List<Role> authorities = new ArrayList<>();
        UserRoleExample userRoleExample = new UserRoleExample();
        userRoleExample.createCriteria().andUserIdEqualTo(id);
        List<UserRole> userRoleList = userRoleMapper.selectByExample(userRoleExample);
        for (UserRole userRole : userRoleList) {
            Role role = roleMapper.selectByPrimaryKey(userRole.getRoleId());
            authorities.add(role);
        }
        return authorities;
    }

自定义配置类

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,jsr250Enabled = true,securedEnabled = true)    //开权限方法权限注解支持
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;
    @Autowired
    private DataSource dataSource;

    @Bean
    public PasswordEncoder createPwdEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl persistentTokenRepository = new JdbcTokenRepositoryImpl();
        persistentTokenRepository.setDataSource(dataSource);
        return persistentTokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()    //对登录请求放行
                .antMatchers("/res/**").permitAll()
                .antMatchers("/index/**").hasAnyAuthority("VIP")
                .antMatchers("/**")   //拦截根目录以及根目录下的子目录
                .fullyAuthenticated()  //对所有的资源进行请求拦截
                .and()
                .formLogin()   //以表单验证的方式对所有的拦截资源进行认证
                .loginPage("/login")   //自定义登录页面
//                .successForwardUrl("/")  //登录成功后的跳转页面
                .successHandler(new MyAuthenticationSuccessHandler())  //登录成功返回json信息
                .failureHandler(new MyAuthenticationFailureHandler())  //登录失败返回json信息
                .and()
                .rememberMe().tokenValiditySeconds(86400)
                .and()
                .logout().deleteCookies("remember-me")
        .and().headers().frameOptions().sameOrigin()
        .and().sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry());
//                .csrf().disable()   //关闭跨域访问
        ;
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }
}

登陆成功返回json

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=utf-8");
        PrintWriter out = httpServletResponse.getWriter();
        out.write(JsonUtils.objectToJson(LayUIResult.build(200,"登陆成功")));
        out.flush();
        out.close();
    }
}

登陆失败返回json

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=utf-8");
        PrintWriter out = httpServletResponse.getWriter();
        out.write(JsonUtils.objectToJson(LayUIResult.build(400,"登录失败")));
        out.flush();
        out.close();
    }
}

自定义login页面

@RequestMapping("/login")
    public String login() {
        return "/page/login/login";
    }

权限验证

springboot配置类形式,在SpringSecurity配置类上添加注解

@EnableGlobalMethodSecurity(prePostEnabled = true,jsr250Enabled = true,securedEnabled = true)    //开权限方法权限注解支持

在controller或者service上开启权限控制

//@PreAuthorize("hasRole('ROLE_MANAGER')")
@Secured({"ROLE_MANAGER","ROLE_USER"})
@RolesAllowed({"ROLE_USER","ROLE_MANAGER"})

只需要开启一个即可,可以实现对controller等的权限控制

csrf跨域访问

跨域访问保护默认是开启的,这时你的登陆、退出必须为POST方式,以及增删改必须携带CSRF令牌

方式一:form表单中携带隐藏令牌

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
$.ajax({
            type: "POST",
            url: "/login",
            data: $('#loginForm').serialize(),
            dataType: "json",
            success: function (loginCallback) {
                if (loginCallback.code == '200') {
                    layer.msg("登录成功", {
                        icon: 6,
                        time: 1000 //2秒关闭(如果不配置,默认是3秒)
                    }, function () {    //弹框后的操作
                        window.location.href = "/";
                    });
                } else {
                    layer.msg("用户名或密码错误,请重新输入", {
                        icon: 2,
                        time: 2000 //2秒关闭(如果不配置,默认是3秒)
                    });
                }
            },
            error: function (jqXHR) {
                layer.alert("发生错误:" + jqXHR.status, {
                    title: 'Error'
                });
            }
        });

方式二:头信息中携带

<meta name="_csrf" th:content="${_csrf.token}"/>
  <meta name="_csrf_header" th:content="${_csrf.headerName}">
//ajax请求时都带上csrf信息
    $(function () {
        var token = $("meta[name='_csrf']").attr("content")
        var header = $("meta[name='_csrf_header']").attr("content")
        $(document).ajaxSend(function (e, xhr, options) {
        xhr.setRequestHeader(header,token)
        })
    })